Loomup Docs
Guide /docs/control-plane

Control plane (multi-tenant foundation)

Loomup’s open-source binary is primarily a single-project server (init + serve). The control plane is the multi-tenant catalog for platform accounts, workspaces, and projects. Hosted projects run in one shared API process while each project maps to an isolated Loomup data root and SQLite database. See shared-hosting.md for the runtime and deployment model.

The control plane exposes every project through a stable /p/<project-id> route, verifies recovery points, rotates project signing keys, and supports local↔managed portability. platform serve loads isolated project contexts and dispatches to them in-process; it does not spawn project processes or ports.

Hierarchy

code
Platform user  (account for the control plane — not app _users)
  └── Workspace
        └── Logical project
              ├── default environment (isolated project directory)
              │     ├── loomup.toml
              │     ├── data/app.sqlite   ← tenant app DB
              │     └── migrations/
              └── staging / preview environments (exact recovery clones)
                    └── schema-only promotion through the shared planner

The control-plane catalog SQLite file is never the tenant app database.

Defaults

PathDefault
Catalog.loomup/control.sqlite
Projects root.loomup/projects/<project-id>/

Override with --catalog and --projects-root.

CLI

bash
# Register a platform account (creates a personal workspace + session token)
loomup platform register \
  --email you@example.com \
  --password 'secret12'

# Or login again later (add --json for scripting)
loomup platform login \
  --email you@example.com \
  --password 'secret12' \
  --json

export LOOMUP_PLATFORM_TOKEN='loomup_…'   # optional instead of --token on each command

# Create a project (provisions isolated init bootstrap)
loomup platform create-project \
  --name my-app \
  --admin-password 'adminpass'

# List / get (shared gateway metadata)
loomup platform whoami
loomup platform status
loomup platform list-workspaces
loomup platform list-projects
loomup platform get-project --id "$PROJECT_ID" --json

# Extra workspaces (beyond the personal one created at register)
loomup platform create-workspace --name "Acme Inc"
loomup platform rename-workspace --id "$WS" --name "Acme Corp"
loomup platform leave-workspace --id "$WS"          # members only
loomup platform delete-workspace --id "$WS"         # owner; must be empty

# Team: invite another registered platform user into your workspace
loomup platform invite-member --workspace "$WS" --email teammate@example.com --role member
loomup platform list-members --workspace "$WS"
loomup platform remove-member --workspace "$WS" --user-id "$USER_ID"
loomup platform transfer-ownership --workspace "$WS" --new-owner "$USER_ID"

# Rename / delete a project (delete: workspace owner or project creator)
loomup platform rename-project --id "$PROJECT_ID" --name "New Name"
loomup platform delete-project --id "$PROJECT_ID"

# Audit (your own control-plane actions) + catalog backup
loomup platform audit --limit 50
loomup platform backup-catalog -o ./control-backup.sqlite

# Account
loomup platform change-password --current-password '…' --new-password '…'
loomup platform logout

# Verified recovery and point-in-time clone/restore
loomup platform backup-project --id "$PROJECT_ID"
loomup platform normalize-managed-ids --id "$PROJECT_ID"
loomup platform normalize-managed-ids --id "$PROJECT_ID" --yes
loomup platform clone-project --id "$PROJECT_ID" --name "Before release" --at-sequence 42
loomup platform restore-project --id "$PROJECT_ID" --at-sequence 42 --yes

# Rotate a project's JWT key under maintenance; no secret is printed
loomup platform rotate-project-secret --id "$PROJECT_ID"

# Detach to normal local artifacts, or attach an existing local project
loomup platform detach-project --id "$PROJECT_ID" --output ./detached
loomup dev --config ./detached/loomup.toml
loomup platform attach-project --id "$PROJECT_ID" --config ./local/loomup.toml --yes

# Isolated environments and schema-only promotion through the shared planner
loomup platform create-environment --id "$PROJECT_ID" --name staging
loomup platform list-environments --id "$PROJECT_ID"
loomup platform promote-environment --id "$PROJECT_ID" --from staging --to default
loomup platform promote-environment --id "$PROJECT_ID" --from staging --to default --yes

# CI/Git checkout deployment entry: dry run first, then apply under maintenance
loomup platform deploy-project --id "$PROJECT_ID" --manifest ./loomup.app.toml
loomup platform deploy-project --id "$PROJECT_ID" --manifest ./loomup.app.toml --yes

# Operator console + stable per-project HTTP/WebSocket gateway behind TLS
loomup platform serve \
  --host 127.0.0.1 --port 9797 \
  --public-base-url https://tryloomup.com \
  --behind-tls-proxy --trust-proxy

Token resolution: --token flag, else LOOMUP_PLATFORM_TOKEN env.

What create-project returns

  • Stable project id
  • data_root, config_path, database_path (absolute)
  • Stable hosted gateway path /p/<project-id> on the common API origin
  • Tenant admin_email (platform email) and a credentials hint
  • Hosted health and generation metadata (there is no tenant PID or port)
  • When public_base_url is configured, the SDK base URL /p/<project-id> (including /realtime)

Tenant app auth (_users inside the project DB) is separate from platform identity.

Workspace API keys

Workspace API keys (lbsk_…) are control-plane credentials for CI and other non-human automation. They can be scoped independently to list/create projects and list/create/revoke project API keys. A key with project_keys:create must also declare a project-scope delegation ceiling; child loomup_sk_… keys cannot request permissions outside that ceiling. Workspace keys never authenticate to project data APIs directly.

The npm CLI always authenticates and provisions against the canonical https://tryloomup.com origin:

bash
loomup auth login
loomup workspaces list
loomup projects create --name my-app --link
loomup project-keys create --name deploy --scope schema:apply

Login prints the account's workspace names and IDs. Project creation selects the only workspace automatically or prompts on an interactive terminal when several are available; scripts pass --workspace. --link records the new hosted project in the current package, and project-key and app-integrity commands then infer that linked project unless --project or LOOMUP_PROJECT_ID overrides it. Accounts without a workspace can run loomup workspaces create --name <name>.

For CI, set LOOMUP_WORKSPACE_API_KEY and pass --workspace explicitly to project list/create commands. Authentication commands intentionally do not accept --url, LOOMUP_URL, or another origin override.

Hosted Studio

The browser flow uses platform identity end to end. Registration creates a personal workspace, /platform/onboarding provisions a starter project, and /platform/projects/<id> opens the live Studio without a second tenant-admin login.

Hosted self-service provisioning is bounded by default to three projects per account and 100 projects in the catalog. All projects share the API listener; there is no running-runtime quota. SQLite connection pools open lazily and release unused connections without changing project availability. Project creation also has a five-attempts-per-hour IP and account limiter. Capacity checks and project provisioning are serialized so concurrent requests cannot exceed those limits.

Studio exposes three project-manager surfaces through /platform/api:

  • GET|PUT /projects/<id>/studio/schema reads and immediately publishes safe manifest revisions with optimistic revision checks.
  • GET|POST /projects/<id>/studio/resources/<resource>/records queries and creates live records. Manager reads accept the public Resource list filter, projection, sort, and pagination contract and return total metadata.
  • GET|PATCH|DELETE /projects/<id>/studio/resources/<resource>/records/<record-id> reads, updates, or deletes one record.

Schema publication creates a verified snapshot and puts only the managed project in maintenance. The browser endpoint drains its in-flight requests, disconnects its WebSockets, and rebuilds its context after the publish. A failed schema or manifest write automatically restores the database, config, and prior manifest. Studio permits additive fields and safe Resource configuration; renames, type changes, field deletion, and other destructive changes remain a reviewed manifest/CLI workflow. Removing a Resource from the Studio manifest unexposes it without dropping its SQLite table.

At Platform startup, every runtime whose persisted desired state is running is reconciled up to the runtime capacity limit. Hosted project configs also trust the loopback gateway, use secure cookies behind HTTPS, and accept the forwarded public origin for account-portal CSRF checks.

Library API

rust
use loomup::{ControlPlane, init_project, InitOptions};

let cp = ControlPlane::open_default(".")?;
let (user, workspace, session) = cp.register("dev@example.com", "password1")?;
let conn = cp.create_project(
    &user,
    &workspace.id,
    "demo",
    "adminpass",
    false, // with sample todos table
)?;
// conn.config_path → loomup serve
let _ = cp.project_health(&conn.project)?;

Self-host still uses init_project / loomup init without opening a catalog.

Isolation guarantees (v1)

  • Each project has its own directory and SQLite file under projects_root.
  • List/get/delete enforce workspace membership; other workspaces’ projects return forbidden / empty.
  • Workspace invite is owner-only; members can list/create within shared workspaces once invited.
  • Project delete is workspace owner or project creator only.
  • Workspace delete requires zero projects (no silent cascade of tenant DBs).
  • Catalog path is never used as a tenant DB path.
  • Login uses constant-cost password verification (dummy hash when email is unknown).
  • Mutating control-plane actions write audit rows (visible to the actor via platform audit).
  • Runtime stop/attach/restore/secret operations validate the project lock and cannot replace a live database.
  • Attach stages and integrity-checks every artifact, creates a verified rollback snapshot, and swaps failure-atomically.
  • Project-schema apply journals its database and file updates, recovers an interrupted apply on startup, and restores the verified snapshot plus prior files if the hosted runtime cannot reload the committed schema.
  • Every schema apply is backed by a durable operation. Prefer: respond-async returns 202 plus its Location; older synchronous clients wait on that same record. The API exposes queued/running/terminal stages without storing the schema credential. Only one operation may be active per project. Interrupted operation records are failed explicitly after startup rollback recovery.
  • Online rollback snapshots advance in bounded SQLite backup steps and fail after 60 seconds. Partial snapshot files are removed, and project maintenance has a 120-second availability budget before the serving slot makes itself ineligible and restarts.
  • Export assigns a fresh local signing key so a detached copy cannot mint tokens accepted by the still-managed project.
  • Managed projects use project-specific secret override names; one process-level JWT environment variable cannot collapse tenant isolation.

Gateway and TLS boundary

The shared data plane binds one host port. Its outer router dispatches normal HTTP and WebSocket traffic under /p/<project-id> directly to the isolated project context, scopes application cookies to that prefix, and rewrites embedded UI root paths. It never bypasses tenant auth or row rules.

Loomup does not pretend to own DNS or ACME credentials it was not given. TLS terminates at the operator's trusted proxy; --behind-tls-proxy and --trust-proxy are explicit, and non-loopback plaintext binds fail closed. The shared gateway makes certificate renewal a single platform-origin concern rather than one port/certificate per tenant.

Recovery and portability contract

  • backup-project uses SQLite's online backup API, integrity-checks the copy, hashes it, and registers its exact journal sequence.
  • normalize-managed-ids is an explicit dry-run-first migration for adopted projects. It converts schema-managed integer IDs and references to text without changing their values, creates a verified rollback snapshot, and blocks when live table columns drift from the stored project schema. Hosted callers use POST /platform/api/projects/{id}/managed-ids/normalize so only the target enters maintenance and reloads.
  • restore-project is maintenance-scoped, creates a pre-restore snapshot, reconstructs an exact sequence into a clone, verifies it, then atomically replaces the managed database.
  • clone-project preserves application rows, IDs, users, manifest, migrations, and storage while assigning a distinct hosted context and secret.
  • export-project/detach-project produce loomup.toml, loomup.app.toml, migrations, storage, and a normal data/app.sqlite that loomup dev can run.
  • attach-project preserves managed paths, quotas, metering, snapshots, and signing trust while importing portable application artifacts.

Deliberate boundary

The control plane provides single-primary managed operations, team access, usage/capacity views, a shared TLS gateway, verified recovery, and portable attach/detach. DNS ownership, CDN policy, billing collection, SSO, and multi-writer claims remain integrations outside the data-plane contract.

Environment creation is an exact recovery clone with its own hosted context, signing key, database, and storage behind the common gateway. Promotion is intentionally schema-only: it runs the same application-manifest planner used by loomup dev, never copies tenant rows or credentials, places only the target in maintenance, and creates a verified rollback point. A Git workflow can call the deploy CLI/API after checkout; Loomup does not need repository credentials in the data plane.