Skip to content
Browse docs

Building a Layerbase App

A Layerbase app is one stateless container plus a managed database. You ship an image; we run it, wire it to a database we provision, give it HTTPS, and self-heal it. Our own Session Replay runs in production exactly this way.

The whole contract, in five rules

Your app mustWhy
Listen on PORT, bound to all interfacesOur proxy terminates TLS and routes to your container
Persist ONLY to DATABASE_URL, and migrate on boot, idempotentlyThe container is disposable: every redeploy replaces it
Answer GET /healthWe probe it and recreate your app when it stops answering
Exit cleanly on SIGTERMRedeploys drain in-flight requests instead of dropping them
Ship as a prebuilt image with an OCI version labelNothing builds on boot; we detect updates from the label

That is the entire platform-facing surface. Everything else - TLS, domains, DNS, the database itself, backups, restarts - is our job, not yours.

Start from the starter

The fastest way to see the contract is to run it:

Layerbase-LLC/layerbase-app-starter is the same minimal todo app implemented twice - once as Node + Vite (a Hono server serving a React SPA) and once as Next.js (App Router, standalone output). Each folder is a few hundred lines, and each README maps every contract rule to the exact file that satisfies it.

  1. Copy the folder that matches your stack.
  2. Run it locally: spindb create todos --engine postgresql, put the connection string in .env, pnpm install && pnpm dev.
  3. Replace the todo parts with your real app. The platform-facing plumbing - port binding, boot migration, health, shutdown, Dockerfile, image publishing - is already done.

What the platform provides

  • A dedicated container for your image, with a hard memory cap.
  • A provisioned database, injected as a connection string. Any connection-string engine we host works (Postgres, MySQL, MariaDB, MongoDB, Redis, libSQL, ClickHouse, and more). URL-only engines (Qdrant, Meilisearch, InfluxDB, Weaviate) get a second env var carrying the provisioned key. The one exception is TigerBeetle: a file-based ledger with no connection string.
  • An HTTPS subdomain (your-app.cloud.layerbase.dev) with managed TLS, and custom domains from the Domain tab: we show the DNS record, verify it, issue the certificate, and switch the app (and anything derived from its URL, like install snippets) to your domain.
  • Self-healing: a reconciler probes /health and recreates a wedged container automatically.
  • A dashboard with no dashboard code: Overview, Domain, Settings, and Install tabs are universal; Docs, About, and Auth light up from your app definition (below).

The free Auth tab

If your app manages logins with Better Auth tables in its backing store, the dashboard recognizes the schema and renders a full Auth tab - list users, create, reset passwords, disable, delete - with zero UI work from you. Opting in is two lines of the definition:

ts
backingStore: { engine: 'postgresql', connectionStringEnv: 'DATABASE_URL' },
authConsole: { enabled: true, store: 'backingStore' },

Works on the auth-console engines: libSQL, Postgres, MySQL, MariaDB.

Rules of the box

  • One process, one image. No docker-compose, no sidecars. This is a security boundary: every listed app is audited, and a single process is what keeps that review tractable. If you need a database, you get one of ours.
  • Memory is a hard cap. Exceed it and the container is stopped.
  • Storage is a quota. Cross it and the backing database flips read-only (reads keep serving, writes pause) until usage drops. Design for this: decide what is disposable and reclaim it on a timer. Session Replay prunes its oldest unpinned recordings by age and by size.

Restricting who can talk to your app

If your app accepts calls from other sites (Session Replay accepts recorder events), gate them two ways, like it does: a write key as the primary check (invalid key = rejected) and an allowed-origins list as a secondary check on the browser Origin. Origins support exact values and wildcards (https://*.example.com) and default to * so recording works immediately; users lock it down from the Settings tab.

Versioning and one-click upgrades

The dashboard compares the OCI version label on your latest published image to the running container's, shows an Update available badge, and upgrades with one click (re-pull + recreate; data and settings unchanged). To participate, a release needs:

  • semver in package.json, gated by check-version CI (no merge without a bump)
  • an immutable per-release tag (:0.2.0) next to the moving :v1 / :latest
  • the org.opencontainers.image.version label baked into the image
  • a public GET /api/version reporting the running build

The starter's publish workflow already does the tagging and labeling.

The app definition

When an app is listed in the catalog, we describe it declaratively - the image, the backing store, the resource caps, the env contract, and the content that fills the Docs and About tabs. Session Replay, abbreviated:

ts
const SESSION_REPLAY = {
  appType: 'session-replay',
  image: { repository: 'ghcr.io/.../session-replay', defaultVersion: 'v1' },
  ports: [{ name: 'http', portEnv: 'PORT', exposure: 'public', primary: true }],
  resources: { memoryMb: 1024, diskMb: 1024, cgroup: 'user-slice' },
  backingStore: { engine: 'postgresql', connectionStringEnv: 'DATABASE_URL' },
  authConsole: { enabled: true, store: 'backingStore' },
  env: {
    PORT:               { kind: 'port' },
    DATABASE_URL:       { kind: 'connectionString' },
    PUBLIC_BASE_URL:    { kind: 'baseUrl' }, // custom domain once verified
    BETTER_AUTH_SECRET: { kind: 'generatedSecret', bytes: 48 },
    RETENTION_DAYS:     { kind: 'userSetting', setting: 'retentionDays', valueType: 'number', default: 0 },
  },
  display: { author: { name: ..., url: ... }, overview: ... }, // -> About tab
  docs: { markdown: ... },                                      // -> Docs tab
}

The env-source kinds: literal, port, baseUrl, connectionString, generatedSecret, and userSetting (typed, user-editable, optional default). You never compute these values; you name where each one goes and the platform fills them in at provision time.

Submissions

App listings are not self-serve yet - today, apps are first-party, and the definition above is written by us when an app is listed. If you have built something on this contract (start from the starter and you have), we want to hear about it: get in touch.