Renoxium Docs Pricing Sign in Sign up

05 — Tenant code lifecycle

Tenant code becomes an immutable artifact referenced by hash. There is no build, npm install, or filesystem access at runtime; all dependencies are frozen at deploy time.

Deploy pipeline

renox deploy
  │
  ├─ 1. CLI bundles the code: TS → JS (swc), npm: and node: imports resolved,
  │     output = one eszip archive + manifest + templates transpiled to minijinja + Cedar policies
  │
  ├─ 2. Upload to the control plane (POST /v1/deployments) with the tenant token
  │
  ├─ 3. Control plane validates: bundle size, manifest, requested permissions
  │     (network, secrets, modules), templates and policies against schemas, route conflicts,
  │     bundle hygiene (forbidden files, secrets; below)
  │
  ├─ 4. Artifact stored in object storage, content-addressed: sha256(bundle). Never overwritten.
  │
  ├─ 5. Deployment recorded in Postgres: tenant, hash, approved permissions, secret version
  │
  └─ 6. Route table updated via pub/sub: tenant hostname → new hash.
        Rollback = move the pointer back to an old hash. No rebuild.

Bundle hygiene

The same rules run in renox build and in the control plane's validation (@renox/artifact-validate, #115), so the artifact store never holds a known secret or a stray private file (docs/24-threat-model.md sections B and C):

  • Forbidden files (H1): dotfiles and dot-directories (.env*, .git/, editor folders) except public/.well-known/; node_modules/ outside the bundle; keys and certificates (*.pem, *.key, *.p12, *.pfx, id_rsa*); databases (*.db, *.sqlite); *.log; symbolic links; public/ files over 5 MiB (those belong in storage). renox build leaves them out of public/ and says so; an upload holding one is rejected.
  • Secrets (H2): private key blocks, AWS access key ids, GitHub tokens, Stripe live keys, Slack tokens and webhooks, Google API keys, unsigned JWTs, and high-entropy strings assigned to names like secret, token, key, password. renox build and renox check --secrets scan the project's own bundled sources and the files copied as they are, and fail with file and line; the control plane scans the files stored as they are (public/, migrations/, templates/). A line that must keep such a string carries renox-allow-secret: <reason>. Findings name the pattern and a masked prefix, never the value.
  • A rejected upload is never written to the artifact bucket.

Artifact contents

PartFormatProduced by
Codeeszip (Deno's module archive) with V8 code cacheCLI (swc, deno bundler)
Templatesminijinja source per page/partial/layout, plus a fragment mapCLI (renox-template transpiler, compiled to Wasm for the CLI or invoked via the embedded worker)
PoliciesCedar policy set + schemaCLI
Manifestrenox.toml resolved + renox.lock + route table + permission requestsCLI
MigrationsSQL files per module with checksumsCopied verbatim
Public assetspublic/ tree with content hashesCopied verbatim; served by the gateway

Routing and cold start

  1. The gateway maps hostname → tenant → deployment hash (in-memory cache, invalidated via pub/sub).
  2. The request is routed to the worker node that owns the tenant's cell (consistent hashing), so the isolate stays warm and the SQLite file is local.
  3. On a cold start, the worker fetches the artifact from its disk cache or object storage, verifies the hash, creates an isolate from the platform snapshot, and loads the tenant module from eszip with the V8 code cache. Target: single-digit to tens of milliseconds.
  4. The isolate stays warm. After an idle timeout (default 10 minutes), the isolate is evicted and the SQLite file closed. This is per-tenant scale-to-zero.

Deployment switch: the new isolate is created alongside the old one; new requests go to the new deployment; in-flight requests finish on the old one; the old isolate is evicted after they drain.

Custom domains and DNS

Every app gets <app>.renoxium.app and per-deployment preview hosts (<app>-<hash>.renoxium.app); renoxium.app serves tenant apps only, and the platform's own services live on renoxium.com (docs/contracts/README.md, "Domains"). Tenants attach their own domains without transferring them; Renox never needs to be the registrar or DNS provider. Routing is by hostname (SNI and Host), so a custom domain is one more row in the gateway's route table.

Attaching a domain

StepTenantRenox
1. AddDashboard, renox domains add www.example.com, or the MCP tool add_domainCreates a verification token and shows DNS instructions
2. Verify ownershipTXT _renox-verify.example.com = rnx-verify-<token>Polls until it matches. The hostname is not served before verification (prevents domain takeover)
3. Point trafficwww → CNAME edge.renoxium.app; apex (example.com) → ALIAS/ANAME where the DNS provider supports it, otherwise A/AAAA records to Renox anycast IPsWatches propagation; shows "DNS: ok / pending"
4. CertificateNothingACME (Let's Encrypt): HTTP-01 for hostnames, DNS-01 for wildcards; renewal 30 days before expiry; issuance queued to respect rate limits
5. LiveOpens https://www.example.comServes with the right certificate; apex ↔ www redirect per the tenant's canonical choice

Supported cases

CaseSupport
Several domains per appYes; one canonical, others 301
Domain proxied by CloudflareYes; certificates still issued; Cloudflare connects to Renox over TLS
Wildcard (*.example.com) for tenants building their own multi-tenant SaaSYes; requires DNS-01 via a one-time CNAME _acme-challenge.example.com → acme.renoxium.app; one certificate covers all subdomains
Domains owned by the tenant's own customers (Shopify-style shopbudi.com → tenant app)Yes, via the Domains API from inside the app: ctx.domains.add("shopbudi.com", { route: "/stores/budi" }); the tenant app shows the DNS instructions to its customer; verification and certificates work the same way; metered per domain
Other records (MX, unrelated subdomains)Untouched; Renox only asks for records of registered hostnames
Buying domains through RenoxNot initially; possible later through a registrar partner

Security

  • Verification before serving; the verification record must stay in place (re-checked periodically) or serving stops after a grace period.
  • Certificates and private keys are stored encrypted in the control plane and pushed to gateways; keys never enter a tenant cell.
  • CAA guidance in the instructions (0 issue "letsencrypt.org").
  • HSTS is enabled after the first certificate; tenants may disable it on custom domains (not on *.renoxium.app).
  • Domains that stop pointing at Renox are flagged and their renewals paused; serving resumes automatically when DNS points back.

Secrets and data access

  • Secrets are injected as frozen objects via a Rust op at isolate creation (04-isolation-security.md).
  • The tenant receives only a db handle bound to its own file. There are no raw paths or connection strings on the tenant side.

Metering and billing

Each invocation emits one record:

tenant_id, deployment_hash, route_or_function, started_at,
cpu_ms, wall_ms, peak_heap_bytes, egress_bytes, db_ms, db_rows, render_fuel, status

Records are batched per cell and shipped to the control plane, where they are aggregated hourly into billing tables. Realtime connections are metered by connection-seconds and messages. Storage is metered by file size at the end of each hour.

Tenant console.log output is streamed to a per-tenant sink with a volume limit; it is visible in the dashboard and through renox logs --follow.

Marketplace modules

  • Publishers sign modules (Ed25519); the registry stores hash and signature; tenants can only install modules whose verification passes.
  • Permissions are reviewed at install ("requests access to api.stripe.com, tables billing__*, slot admin.sidebar"), and permission diffs must be approved again on upgrade.
  • Marketplace modules run in isolate mode and see no tenant secrets except those declared and approved.
  • A module hash proven malicious can be disabled globally; tenants using it are notified.

Lifecycle states

StateMeaningTransition
uploadedBundle received, not yet validated→ validated or rejected
validatedPassed validation, artifact stored→ active on activation
activeRoute table points here→ superseded when a newer deployment is activated
supersededRetained for rollback→ active on rollback; → pruned after the retention limit
suspendedTenant suspended (billing, abuse)→ active on reinstatement

What we build vs what exists

ComponentStatus
Isolates, snapshots, heap/CPU limits, eszip, npm:/node:Exists in Supabase Edge Runtime / deno_runtime
Process sandbox (seccomp, cgroups), process tiers, microVMOS configuration + orchestration; examples from Cloudflare and Fly.io, but must be assembled
Control plane: deploy API, artifact store, route table, secrets, billingMust be built. This is the largest part of our work and the part that differentiates the product
Module registry, signatures, permission reviewMust be built, later phase

Source on GitHub