Skip to content

Deployment

Configuration

All deployment settings live in one JSON file, not in environment variables. Each binary reads --config <path> if given, else integraal.json in its working directory. For Lambda, put integraal.json in the zip next to bootstrap. Unknown keys are rejected and a bad file stops the process at startup.

{
  "scripts":   { "bucket": "scripts", "prefix": "" },
  "buckets":   { "prefix": "tenants/", "map": { "In": "acme-in", "Out": "acme-out", "Backup": "acme-bak", "Temp": "acme-tmp" } },
  "batch":     { "bucket": "partner-batch", "prefix": "tenants/" },
  "db":        { "table": "integraal" },
  "callbacks": { "base_url": "https://hooks.example.com" },
  "crypto":    { "keys": { "1": "INTEGRAAL_KEY_1" }, "active": 1 },
  "admin":     { "port": 9090, "token_env": "INTEGRAAL_ADMIN_TOKEN" },
  "queue":     { "url": "https://sqs.eu-west-1.amazonaws.com/123456789012/integraal" },
  "server":    { "port": 8080 },
  "throttle":  [{ "url": "https://api.partner.com/v1/", "max_concurrent": 2, "per_second": 5 }],
  "ceiling":   { "timeout_ms": 60000, "memory_mb": 256 },
  "log":       "info"
}
Key Used by Meaning
scripts.bucket all (required) bucket holding scripts and tenant config.json files
scripts.prefix all key prefix, concatenated as-is (end it with /)
buckets.map server, Lambda named buckets (case-sensitive letters, digits and _; by convention In, Out, Backup, Temp). Enables native.bucket.<name>; see Buckets
buckets.prefix server, Lambda base prefix applied in every bucket; empty or ending in /. Tenant data lives at <prefix><tenant>/
batch.bucket, batch.prefix server, Lambda the dedicated bucket behind native.batch, kept apart from buckets.map; its S3 notifications go to the same queue. See Batch jobs
db.table all DynamoDB table behind native.db and native.callback; see DynamoDB
callbacks.base_url server, Lambda public base URL of the webhook Lambda; enables native.callback. Requires db; see Callbacks
crypto.keys, crypto.active server, Lambda enables native.crypto; see Crypto
admin.port, admin.token_env server operator API for the credential vault on its own port. Requires db and crypto
queue.url server, webhook, Lambda the server polls it (and splits S3 event notifications from the buckets.map buckets, so it also needs sqs:SendMessage to re-send failed records); the webhook Lambda sends to it, and so does native.queue.send for tenants that enable it
server.port server HTTP port (default 8080)
throttle server outbound HTTP limits, see Throttling
ceiling server, Lambda operator ceilings for tenant timeout_ms and memory_mb; see Limits
log all tracing filter, such as info or integraal_core=debug (default info)

Apart from the standard AWS SDK variables (credentials, region and AWS_ENDPOINT_URL, to point at a simulator such as roto), the only environment variables are the standard OTEL_* ones, which turn on OpenTelemetry export.

Credential vault

Secrets (API keys, client secrets) that scripts may send in outbound HTTP but never read; see native.http. They are stored one row each in the db.table under a partition scripts cannot address (VAULT#<tenant>), sealed with a key derived from the master keys and bound to the tenant and name. Needs db and crypto.

Operators manage them through the server's admin API, enabled by admin. It listens on its own port: keep that port out of the load balancer and the security group, and reach it from a bastion or an internal network. admin.token_env names the environment variable holding the bearer token (at least 24 characters, e.g. openssl rand -hex 32).

curl -X PUT  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
     -d '{"value":{"live":{"key":"sk_live_..."}},"allow":["https://api.stripe.com/v1/"]}' \
     http://server:9090/admin/vault/acme/stripe
curl         -H "Authorization: Bearer $TOKEN" http://server:9090/admin/vault/acme      # names + allow, no values
curl -X DELETE -H "Authorization: Bearer $TOKEN" http://server:9090/admin/vault/acme/stripe
curl -X POST   -H "Authorization: Bearer $TOKEN" http://server:9090/admin/rekey          # after a master key rotation

allow (1-16 prefixes, same syntax as http.allow) limits where that secret can ever be sent, independently of the tenant's own allowlist. "stream_unmasked": true (default false) lets requests using the secret set stream = true; the streamed body is then not scanned for the value, so set it only when the service at the allow URLs never echoes request data, and keep the prefixes narrow. A value is any JSON (not null) up to 16 KB, so one secret can hold several related fields; scripts address a field as {{vault:name.field.path}}. Names are 1-64 characters (letters, digits, -, _; no ., which separates the path). Writes and deletes are logged with tenant and name, never the value.

Rotation. A small index (SYS#vault-index, one row per tenant that has stored a secret) lets POST /admin/rekey re-seal every tenant's entries under the active master key; it is idempotent, so repeat it after a failure. POST /admin/vault/{tenant}/rekey does one tenant. Once it reports nothing left to re-seal, the old master key can be removed. The index and the per-tenant lists are read in one query each, so rotation covers up to 999 tenants and 999 secrets per tenant and fails loudly beyond that.

Bucket layout

<scripts.bucket>/<tenant>/<script>/latest.luau      mutable, revalidated
<scripts.bucket>/<tenant>/<script>/<version>.luau   immutable
<scripts.bucket>/<tenant>/config.json               operator-owned policy
<each buckets.map bucket>/<buckets.prefix><tenant>/...   tenant data, see Buckets
<batch.bucket>/<batch.prefix><tenant>/jobs|state/...      batch jobs, see Batch jobs

IAM

Per-feature permissions are listed in each chapter: Buckets, DynamoDB, Callbacks. In addition the runtime role needs s3:GetObject on the script bucket and, for the server, sqs:ReceiveMessage/DeleteMessage on the queue. Give tenants no direct access to the script bucket.

Lambda

Build the bootstrap binary for arm64 and use the provided.al2023 runtime:

cargo install cargo-lambda
cargo lambda build --release --arm64 -p integraal-lambda

Fargate

Build integraal-server into a container image and run it behind an ALB (target group health check GET /healthz). Add queue.url to have the same task consume a queue.

Note

Infrastructure-as-code (CDK/Terraform) is not part of the repository yet.