/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 provider boundary for Stripe or another billing system
Local data-plane provisioner
SettingLIP_CLOUD_PROGRAM_DIR starts a provisioning worker with LocalDataPlaneProvisioner, which runs one LIP data-plane runtime per environment inside the control-plane process. Each create job:
1
Load the program
Reads
<program_id>.json from the program directory.2
Start an isolated runtime
Per-environment SQLite under
LIP_CLOUD_DATA_DIR (default .lip-cloud), or tenant-scoped Postgres when LIP_CLOUD_DATA_PLANE_DATABASE_URL is set.3
Allocate a stable port
From
LIP_CLOUD_DATA_PLANE_BASE_PORT (default 13210), recorded in <data-dir>/ports.json.4
Bootstrap a merchant API key
An owner-role key minted through the tenant’s own access-control service — hashed, audited, rotatable — written to a
0600 credentials file.5
Mark the environment ready
With its reachable
api_url and admin_url.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 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.
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 TLS (or is a localhost address for local development)
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.
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.