Skip to content

Infrastructure as Code Deep Dive

A comprehensive guide to managing MolnOS infrastructure declaratively with molnos.json.

MolnOS IaC is not Terraform. It’s closer to kubectl apply or docker compose—a thin declarative layer over the MolnOS control plane API.

Key differences from traditional IaC:

AspectTraditional IaCMolnOS IaC
StateLocal state fileLive control plane
ExecutionPlan → ApplyDirect apply
ComplexityDAG solver, providersSimple reconciliation
ScopeMulti-cloudMolnOS only

The file is the desired state. MolnOS is the actual state. Apply reconciles them.

Every molnos.json follows this structure:

{
"$schema": "https://schemas.molnos.cloud/schema-iac-v1.json",
"version": "1",
"context": {
"name": "my-service",
"intent": "Description of what this context does."
},
"resources": {
"functions": { },
"databases": { },
"storage": { },
"sites": { },
"applications": { },
"schemas": { }
},
"identities": {
"users": { },
"serviceAccounts": { }
}
}
  • version - Schema version (currently "1")
  • $schema - JSON Schema URL for editor support (autocomplete, validation)
  • context - Context metadata (defaults to default context)
  • resources - Resource definitions
  • identities - User and service account definitions

A context is a logical grouping of related resources—an application, service, or workload.

  1. Ownership: Every resource belongs to exactly one context
  2. Safe Pruning: --prune only affects resources in the current context
  3. Isolation: Different teams can manage different contexts independently
  4. Lifecycle: Destroy a context to remove all its resources
{
"context": {
"name": "billing-service",
"intent": "Handles invoicing and payment processing for EU customers.",
"attributes": {
"data": {
"sensitivity": "high",
"residency": "eu"
},
"availability": "business-critical",
"regulatory": ["gdpr"],
"owner": "[email protected]"
}
}
}

Fields:

  • name - Unique identifier for the context
  • intent - Human-readable description of purpose
  • attributes - Freeform metadata (not enforced, but queryable)

If you omit context, resources go to the default context:

{
"version": "1",
"resources": {
"databases": {
"scratch-data": {}
}
}
}

This is equivalent to "context": { "name": "default" }.

To completely remove a context and all its resources, use the two-step process:

Terminal window
# Step 1: Delete all resources in the context
DELETE /contexts/billing-service/resources
# Step 2: Delete the context metadata
DELETE /contexts/billing-service

Or via CLI:

Terminal window
# Delete resources first, then the context
molnos context delete-resources billing-service
molnos context delete billing-service
# Or use destroy to do both in one command
molnos context destroy billing-service

The delete-resources operation returns a detailed result showing what was deleted:

{
"success": true,
"context": "billing-service",
"deleted": [
{ "type": "function", "name": "invoice-api" },
{ "type": "database", "name": "invoices" },
{ "type": "storage", "name": "pdfs" }
],
"errors": [],
"summary": {
"totalResources": 3,
"deletedCount": 3,
"failedCount": 0
}
}

If some resources fail to delete, success will be false and HTTP status will be 207 Multi-Status. The errors array will contain details about what failed.

Cannot Delete Default Context

The default context cannot be deleted. You can delete its resources, but the context itself will remain.

{
"functions": {
"invoice-api": {
"source": "./dist/invoice.js",
"methods": ["GET", "POST", "PUT"],
"triggers": [
{ "type": "http" },
{ "type": "event", "eventName": "invoice-requested" }
],
"allowUnauthenticated": false,
"passAllHeaders": false,
"bindings": [
{ "service": "databases", "resource": "invoices", "permissions": ["read", "write"] },
{ "service": "storage", "resource": "pdfs", "permissions": ["read"] }
]
}
}
}

Fields:

FieldRequiredDescription
source*Path to pre-built JavaScript file (for CLI use)
code*Inline function code (for API use)
methodsNoAllowed HTTP methods (default: all)
triggersNoArray of trigger configs: { type: "http" } or { type: "event", eventName: "..." }
allowUnauthenticatedNoAllow public access (default: false)
passAllHeadersNoPass internal headers (default: false)
bindingsNoService bindings for resource access

* Either source or code is required. When using the CLI, specify source and the CLI will read the file and send code to the API. When calling the API directly, provide code with the inline function content.

Important: Source files must be pre-built. MolnOS does not run build tools.

{
"databases": {
"invoices": {},
"customers": {},
"audit-log": {}
}
}

Tables are defined by name only. Empty object {} declares the table exists.

MolnOS uses PikoDB—no schema definition, no tuning parameters.

{
"storage": {
"invoice-pdfs": {
"public": false
},
"static-assets": {
"public": true
}
}
}

Fields:

FieldRequiredDescription
publicNoWhether bucket is publicly accessible (default: false)
{
"sites": {
"billing-dashboard": {
"source": "./dist/site"
}
}
}

Fields:

FieldRequiredDescription
source*Path to directory containing static site files (for CLI use)
files*Array of { path, content } objects with base64-encoded content (for API use)

* Either source or files is required. When using the CLI, specify source and the CLI will read the directory, base64-encode file content, and send files to the API. When calling the API directly, provide files with base64-encoded content.

The directory is content-addressed. Uploads are skipped if content hasn’t changed.

{
"resources": {
"applications": {
"inbox-app": {
"description": "MolnOS Inbox Application",
"redirectUris": ["http://localhost:3000/auth-callback"]
}
}
}
}

Fields:

FieldRequiredDescription
descriptionNoHuman-readable description of the application
redirectUrisYesAllowed OAuth redirect URIs for the application

Natural key: The object key (application name)

{
"resources": {
"schemas": {
"order-created": {
"description": "Emitted when a new order is placed",
"schema": {
"properties": {
"orderId": { "type": "string" },
"amount": { "type": "number" },
"currency": { "type": "string" }
},
"required": ["orderId", "amount", "currency"],
"additionalProperties": false
}
}
}
}
}

Fields:

FieldRequiredDescription
descriptionNoHuman-readable description of the schema
schemaYesSchema definition with properties, optional required, and additionalProperties

Natural key: The object key (schema name, must be kebab-case)

Schemas are always considered for update on apply—if the schema exists, the definition is updated and the version is incremented automatically.

{
"identities": {
"users": {
"billing-admin": {
"email": "[email protected]",
"roles": ["administrator"]
},
"support-agent": {
"email": "[email protected]",
"roles": ["user"]
}
}
}
}

Natural key: email

{
"identities": {
"serviceAccounts": {
"invoice-processor": {
"roles": ["user"],
"description": "Automated invoice processing"
},
"backup-agent": {
"roles": ["user"],
"description": "Nightly backup automation"
}
}
}
}

Natural key: The object key (e.g., invoice-processor)

Bindings reference resources by name:

{
"bindings": [
{ "service": "databases", "resource": "invoices", "permissions": ["read", "write"] }
]
}

The invoices database must be defined in the same context.

For resources in other contexts, use qualified names:

{
"bindings": [
{ "service": "databases", "resource": "shared-data:audit-log", "permissions": ["write"] }
]
}

Format: context-name:resource-name

Cross-context references require the target resource to already exist. IaC does not create resources in other contexts.

{
"permissions": ["read", "write", "delete"]
}

Available permissions vary by service:

ServicePermissions
databasesread, write, delete
storageread, write, delete
Terminal window
# Validate without applying
molnos validate
molnos validate path/to/molnos.json
# Show what would change
molnos diff
molnos diff path/to/molnos.json
# Apply configuration
molnos apply
molnos apply path/to/molnos.json
# Apply and prune orphaned resources
molnos apply --prune
# Apply to specific endpoint
molnos apply --endpoint http://localhost:8080

Checks JSON syntax and schema compliance. Does not contact MolnOS.

Catches:

  • Invalid JSON
  • Missing required fields
  • Invalid resource configurations
  • Binding references to undefined resources (warning)

Shows what would change without modifying anything:

+ database: invoices (create)
~ function: invoice-api (update)
- storage: old-bucket (delete, requires --prune)
= database: customers (unchanged)

For each resource type:

  1. Query live state by natural key
  2. If missing → create
  3. If exists → update
  4. If --prune and extra resources exist → delete

--prune is context-scoped. It only removes resources that:

  • Belong to the current context
  • Are not declared in the IaC file

Resources in other contexts are never affected.

Each resource type has a natural key used for matching:

Resource TypeNatural Key
Functionname (object key)
Databasetable name (object key)
Storage Bucketbucket name (object key)
SiteprojectId (object key)
Applicationname (object key)
Schemaname (object key)
Useremail
Service Accountname (object key)

MolnOS IaC is stateless. The reconciliation algorithm:

  1. Read desired state from molnos.json
  2. Read actual state from live MolnOS control plane
  3. Compute delta
  4. Apply changes

There’s no local state to corrupt, sync, or version.

Drift is resolved by overwrite. If live state differs from declared state:

  • The declared state wins
  • MolnOS updates the resource to match the file

This is intentional. The file is the source of truth.

Keep related resources together:

{
"context": { "name": "user-service" },
"resources": {
"functions": { "user-api": { } },
"databases": { "users": {}, "sessions": {} }
}
}

Document purpose for future maintainers:

{
"context": {
"name": "billing-service",
"intent": "Handles invoicing. Processes ~10k invoices/month.",
"attributes": {
"owner": "[email protected]",
"data": { "sensitivity": "high" }
}
}
}

Store molnos.json alongside application code:

my-service/
├── src/
├── dist/
├── molnos.json
└── package.json

Always build your functions and sites before applying:

Terminal window
npm run build
molnos apply

MolnOS does not run build tools.

Add validation to your CI pipeline:

Terminal window
molnos validate molnos.json

Request only the permissions you need:

{
"bindings": [
{ "service": "databases", "resource": "users", "permissions": ["read"] }
]
}

Not:

{
"bindings": [
{ "service": "databases", "resource": "users", "permissions": ["read", "write", "delete"] }
]
}

Use different contexts or files per environment:

Terminal window
molnos apply molnos.cloud.json
molnos apply molnos.prod.json

Or same file, different endpoints:

Terminal window
molnos apply --endpoint http://dev.internal:3000
molnos apply --endpoint http://prod.internal:3000

Check JSON syntax:

Terminal window
cat molnos.json | jq .

Validate against schema:

Terminal window
molnos validate

Bindings reference resources by name. Ensure the target exists:

{
"databases": {
"invoices": {}
},
"functions": {
"api": {
"bindings": [
{ "service": "databases", "resource": "invoices", "permissions": ["read"] }
]
}
}
}

Check your shell environment before applying:

Terminal window
printenv | grep STRIPE
export STRIPE_API_KEY=sk_test_...
molnos apply

Ensure source files are built:

Terminal window
npm run build
ls -la dist/
molnos apply

--prune only affects the current context. Check which context you’re applying:

Terminal window
molnos diff

Resources marked for deletion should only be those in your context that aren’t in the file.

Cross-context references require the target to exist first:

Terminal window
# First, apply the shared context
molnos apply shared-data.json
# Then apply the dependent context
molnos apply billing-service.json