/lip/v1: the protocol stays portable and self-hostable, while the control plane manages organizations, projects, environments, plans, subscriptions, provisioning, and usage limits.
You can leave Cloud and run the same LIP data plane yourself. Cloud earns its keep by removing operational work and risk, not by protocol lock-in.
Product boundary
Open source
Protocol, engine, SDKs, CLI, MCP, Admin UI, SQLite and Postgres storage, Docker deployment, and conformance tooling.
Cloud
One-click environments, upgrades, backups, monitoring, regional operation, usage billing, managed messaging, and support.
Enterprise
SSO/SCIM, dedicated infrastructure, private networking, contractual SLAs, data residency, and migration assistance.
Current vertical slice
The@loyalty-interchange/cloud workspace includes:
- Tenant-safe organizations, issuer/subject identities, invitations, and membership management
- Projects, and development, staging, or production environments
- Generated
tenant_idand configuredprogram_iddata-plane scopes - Queued provisioning records for each environment
- A claim-safe provisioning worker with retries and a provider interface
- Free, Pro, and Business plan definitions, and one subscription per organization
- Idempotent monthly usage events and counters, with hard quota enforcement under a transaction lock
- An authenticated management API with operator-key or direct OIDC modes
- A Stripe adapter with signed webhook verification and a provider boundary for alternate billing systems
Managed diskless runtime
SettingLIP_CLOUD_PUBLIC_BASE_URL starts the diskless managed runtime: one listener, path-scoped tenant runtimes, and every durable byte in Postgres. See the diskless managed runtime runbook. Each create job:
1
Create a bootstrap program
Valid but inert — USD, zero earn rate, no rewards, tiers, plans, members or seeded activity — written to Postgres. Nothing can accrue or redeem until the merchant publishes a real program through the Admin API.
2
Start a tenant runtime
Tenant-scoped rows in the shared database, under forced row-level security. No SQLite file, no disk.
3
Mount it by path
/runtime/v1/environments/<environment_id> on the shared listener. The environment id is the address, so there is no port to allocate and nothing to remember locally.4
Mark the environment ready
With
api_url and admin_url both set to that path-scoped base URL.LIP_CLOUD_PROGRAM_DIR instead starts the standalone file-backed provisioner, which runs one runtime per environment on its own port with per-environment SQLite, credential files and a port registry under LIP_CLOUD_DATA_DIR. It is for standalone Docker and local development. Setting both is refused at boot.
Retrieving and rotating merchant credentials
Merchant credentials are retrieved and rotated through the control plane withPOST /cloud/v1/environments/{id}/credentials/rotate, available to a platform-admin operator, an org-scoped operator covering that organization, or an org owner/admin. Rotation is audited cloud-side, and tenant-side as actor cloud:<subject> where the subject is the verified operator identity.
An Idempotency-Key header is required. The merchant secret is returned once and stored nowhere it can be read back, so a retry without a stable key mints a second live owner key that nobody receives; with one, the same request is served the original credential for 24 hours, a different request under the same key is refused with 409 idempotency_conflict, and an expired handoff answers 410 credential_handoff_expired.
An optional body {"overlap_seconds": <0..604800>} controls the cutover: 0 is an emergency immediate cutover, and the default keeps the replaced key valid for 24 hours. The response includes replaced_api_key_expires_at, the moment the previous merchant key stops working.
The root runtime key is never returned by any API. Tenants can also self-rotate any of their keys via
POST /admin/api/v1/access/api-keys/rotate on their runtime; a replacement inherits the rotated key’s expiry, and an explicit expires_at may shorten it but never extend it. If a tenant self-rotates the merchant key, the control-plane rotation surface recovers by re-adopting the live cloud-merchant lineage rather than failing.provisionTenant and npm run cloud:provision accept an optional --webhook-url plus --webhook-secret (16 characters or more) pair that creates the tenant’s first subscription through the runtime’s admin API at provision time. The subscription id is stable, so re-runs upsert; the merchant credential is minted as a side effect and returned in the result.
Restart behavior
On startup the provisioner callsrestore() and relaunches every credentialed environment on the same port and API key, so BFF LIP_URL values survive restarts. A weak, tampered, or unreadable credentials file is skipped with a cloud_environment_restore_failed log line instead of aborting the other tenants’ restore.
Set LIP_CLOUD_DATA_PLANE_HOST to control the bind address, and LIP_CLOUD_DATA_PLANE_PUBLIC_HOST to control the hostname written into each runtime’s api_url — a private-network service name, for example.
LIP_CLOUD_CREDENTIAL_KEY is a required 32-byte base64url secret whenever the
local provisioner runs. Plaintext v1/v2 files are rejected unless the one-time
LIP_CLOUD_ALLOW_LEGACY_CREDENTIAL_MIGRATION=true switch is set; a successful
restore immediately rewrites them encrypted.
Local SQLite environments support audited
/operations/suspend, /operations/resume, /operations/backup, and
/operations/restore. Backup quiesces then resumes the runtime and writes a
private checksummed encrypted artifact. Restore verifies checksum, safe ids,
authenticated encryption, and metadata before atomically replacing data.
Postgres backup is deliberately refused here; use provider PITR. Regional
adapters still replace local mode in production.
Managed customer routes require
LIP_CLOUD_CUSTOMER_OIDC_ISSUER, LIP_CLOUD_CUSTOMER_TENANT_ID, and
LIP_CLOUD_CUSTOMER_PROVIDER_ID, plus either
LIP_CLOUD_CUSTOMER_OIDC_AUDIENCE or the comma-separated
LIP_CLOUD_CUSTOMER_AUTHORIZED_PARTIES. The audience/authorized-party gate
prevents a valid token minted for an unrelated client from crossing this BFF
boundary.
Attaching a data-plane host
POST /cloud/v1/environments/{environment_id}/attach binds an environment to a LIP data-plane host you run yourself — anywhere, on any infrastructure — with no cloud-provider API involved. It is the remote counterpart to the in-process provisioner: instead of provisioning a runtime, the control plane validates and records a host you already run.
- The URL uses public HTTPS, has no credentials, query, or fragment, and its DNS answers contain no private or reserved address
GET /healthresponds and reportsstatus: "ok"GET /.well-known/lipmatches the expected protocol version and profile- The supplied
api_keyauthenticates againstGET /lip/v1/capabilities, and an unknown key is correctly rejected POST /lip/v1/programs/getconfirms the host serves the environment’sprogram_id
ready with its api_url, admin_url, and an api_key_fingerprint — only a masked fingerprint is stored, never the key. On failure it is marked failed with a status_message naming the failed check, and the request returns 422 with a matching code such as auth_rejected or program_mismatch. Requests have a five-second timeout and never follow redirects.
Re-attaching is allowed for pending, ready, or failed environments, so you can rebind after key rotation or a host migration. A suspended environment rejects attach with 409 environment_suspended.
Verifying a staging tenant
Attach binds the host, but binding is not proof the tenant is safe to send traffic to. After/attach returns ready, run the same diagnostics used to gate a local sandbox:
cloud-verify runs lip doctor (discovery, health, authentication, capabilities) and baseline conformance, then — given --program-id, --expect-member, and --expect-available — looks up that member’s balance and compares it to the expected value. The optional --expect-members also checks the total member count.
The command exits non-zero on any failure, so it can gate promoting a newly attached tenant rather than relying on /attach having returned 200.
--expect-members reads the count from the host’s admin snapshot endpoint, a non-normative operational surface outside the versioned /lip/v1 protocol. Treat it as an operator convenience for staging verification, not a protocol guarantee. lip state import reports the same number from an independent source.Start locally
http://127.0.0.1:3220. From source:
Configuration
Authentication boundary
Operator API keys (primary)
Every human or service operating the control plane has an operator record —platform-admin, or org-scoped with an explicit organization list — and authenticates with a personal key:
expires_at, and rotate with the same bounded-overlap semantics as tenant keys via POST /cloud/v1/operators/{id}/keys/rotate (platform-admin only); a replacement inherits and can never extend the rotated key’s expiry.
Platform-admins are unrestricted; org-scoped operators can only touch their own organizations’ projects and environments. An X-LIP-Cloud-Subject header sent alongside an operator key is recorded in audit metadata as on_behalf_of — it never grants authority.
Bootstrap the first platform-admin with npm run cloud:operator -- create, authenticated by the legacy shared key exactly once, then migrate every caller to LIP_CLOUD_OPERATOR_KEY.
OIDC bearer mode
Production can validate OIDC access tokens directly. ConfigureLIP_CLOUD_OIDC_ISSUER and LIP_CLOUD_OIDC_AUDIENCE together; signature, issuer, audience, expiry, allowed algorithm, and subject are validated against the provider JWKS. Invitation acceptance only uses the email claim when email_verified is true.
When the verified sub matches an active operator record, the token carries that operator’s role and scope. Other verified subjects act as ordinary invitation-based organization members.
Legacy shared key (retired)
The old trusted-gateway mode —Authorization: Bearer <LIP_CLOUD_API_KEY> plus a caller-chosen X-LIP-Cloud-Subject — no longer grants identity. The shared key now authenticates exactly one thing: creating the first operator, and only while zero operators exist.
Every other route, and every use once an operator exists, returns 401 shared_key_retired. Setting LIP_CLOUD_SHARED_KEY_DISABLED=true additionally closes the bootstrap route, returning 401 shared_key_disabled. A single cloud_shared_key_deprecated notice is logged once per boot while the key is still configured.
API
All successful payloads use adata envelope; errors use RFC 9457 problem details.
Isolation and metering
Every project belongs to one organization, every environment belongs to one project, and each environment receives a uniquetenant_id. Repository queries resolve ownership before writes.
Usage writes:
- Lock the environment, metric, and month
- Verify the environment belongs to the expected organization
- Deduplicate by environment, metric, and idempotency key
- Enforce the plan hard limit
- Insert the immutable event and update its monthly counter atomically
Next steps
PostgreSQL storage
The multi-instance engine store the control plane provisions against.
Reference platform
What each provisioned data-plane runtime actually serves.