Skip to main content
Loyalty Interchange Cloud is the managed-service layer around the open protocol and engine. It is intentionally separate from /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_id and configured program_id data-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
The control plane does not yet collect payment. New organizations use the manual billing provider on the Free plan, and new environments stay pending until a provisioning adapter processes their job. A local adapter ships today; regional infrastructure adapters remain future work.

Local data-plane provisioner

Setting LIP_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 with POST /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.
New tenants start with zero webhook subscriptions. 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 calls restore() 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.
Only create operations are supported, and credentials remain files (0600, written atomically) rather than an encrypted secret store. Regional adapters still replace this for production.

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.
Attach is synchronous — no job is queued. The control plane runs five checks before binding:
  1. The URL uses TLS (or is a localhost address for local development)
  2. GET /health responds and reports status: "ok"
  3. GET /.well-known/lip matches the expected protocol version and profile
  4. The supplied api_key authenticates against GET /lip/v1/capabilities, and an unknown key is correctly rejected
  5. POST /lip/v1/programs/get confirms the host serves the environment’s program_id
On success the environment moves to 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

The management API listens on http://127.0.0.1:3220. From source:

Configuration

Authentication boundary

The acting identity on /cloud/v1 always comes from a verified credential — never from a caller-chosen header.

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:
The resolved operator record supplies the subject. Keys are sha256-hashed at rest, support 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. Configure LIP_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.
Never expose a control-plane key directly to browsers or mobile applications.

API

All successful payloads use a data 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 unique tenant_id. Repository queries resolve ownership before writes. Usage writes:
  1. Lock the environment, metric, and month
  2. Verify the environment belongs to the expected organization
  3. Deduplicate by environment, metric, and idempotency key
  4. Enforce the plan hard limit
  5. Insert the immutable event and update its monthly counter atomically
Charging by monthly active members and transactions is represented directly; points issued is not a billing metric.

Next steps

PostgreSQL storage

The multi-instance engine store the control plane provisions against.

Reference platform

What each provisioned data-plane runtime actually serves.