Cloud|

Platform start and connections

Use native database and cache clients after Cloud initializes the application.

6 min read Updated 2026-10-30 #startup#connections#sql#redis

Import server connections from @k2b/cloud:

ts
import { sql, redis, lazySync, storage } from "@k2b/cloud";

const rows = await sql`SELECT id FROM inventory.items`;
await redis.set("inventory:count", String(rows.length), "EX", "60");

The standard build and development preload initialize the platform before loading application code. Build-time configuration discovery stays offline. Keep declarations free of database queries and other connection use.

After platform start, sql is a native Bun SQL instance and redis is a native Bun RedisClient. They retain the complete native API, including transactions, reservations, query helpers, SQL files, listeners, client options, and writable callbacks. Connections open on use.

Before start, using either handle throws sql used before platform start or redis used before platform start. The logger still writes to the console when database persistence fails before startup or after teardown; logging never throws a client startup error. Unit and render tests need no startup when they do not use connections. Tests that replace services can stub their connection snapshot without opening infrastructure connections.

Use lazySync() for module-scope Sync declarations and use them after app.start(). See Background work. storage uses the operator's saved storage settings; see Object store.

Create a separate pool

Use createSql(options?: SqlPoolOptions): SQL or createRedis(options?: RedisOptions): RedisClient from @k2b/cloud when a feature needs its own pool size, timeouts, or offline-queue policy. Import native types from bun; import sql and redis values from @k2b/cloud.

ts
import { createSql, createRedis } from "@k2b/cloud";

const queries = createSql({ max: 4 });
const cache = createRedis({ enableOfflineQueue: false });

These clients use the initialized installation connections. The application owns their lifecycle: close them when the work or its lifecycle ends. SQL connections use application_name=cloud:<app-id> unless the URL supplies an operator name or startup options, or the pool's connection options override it. Bun sends the name in the PostgreSQL startup packet; Cloud issues no per-transaction SET application_name. With PgBouncer transaction pooling, PgBouncer tracks application_name itself and can issue server-side SET commands when reassigning a backend between differently named clients. Setting the startup parameter does not remove that pooler work. application_name is tracked by PgBouncer by default; it does not need an extra track_extra_parameters entry. Preserve per-application names when diagnosing SQL; changing pool separation or dropping that identity is an operator decision. Platform initialization leaves the environment URL unchanged; Bun-global clients get no application name from Cloud.

Migrate existing imports

Move sql and redis value imports from bun to @k2b/cloud. Keep native SQL and RedisClient type imports from bun. Replace owned client constructors with createSql() or createRedis(), preserving their options and closing them through the same lifecycle.

In a Cloud source checkout, preview and apply the migration to one package:

sh
bun scripts/codemods/platform-clients.ts packages/my-app --dry-run
bun scripts/codemods/platform-clients.ts packages/my-app

The command prints changed-file and converted-pool counts. It preserves aliases and unrelated Bun imports; running it again leaves migrated files unchanged. Review explicit Redis URLs, constructor spies, and subprocess startup manually. Standalone repository data commands initialize the same platform clients and need DATABASE_URL and REDIS_URL, including commands that query only Postgres. The platform-clients repository check also catches native module aliases, computed literal keys, and constructor values passed to other functions. Type-only constructor use and SQL.PostgresError classification stay allowed. Native constructors and direct target reads remain available in tests/, scripts/fixtures/, package-root test/ and bench/ directories, test files, and explicitly listed fixture files. A fixtures/ or tests/ directory inside application source does not grant an exemption.

Start a custom data process

Standard application entrypoints need no additional startup call. A separate server or data worker initializes before loading its domain module:

ts
import { startPlatform } from "@k2b/cloud/start";

const application = await startPlatform({
  appId: "inventory",
  load: () => import("./application"),
});
export default application.default;

startPlatform<T>({ appId, load, source? }): Promise<T> initializes connections and native clients, then returns load()'s module. For a worker with no module loader, initializePlatform({ appId, source? }): Promise<void> uses the same initializer. Each process initializes once; a second call throws Platform has already started. Core and Gateway always use source: "env", selected by their application ID, and refuse app keys. Explicit source: "env" also refuses an app key. Repository test subprocesses can inherit the integration initializer through BUN_OPTIONS. After selecting a different test target, use the reset-aware initializeTestPlatform() fixture before domain work; do not call the production initializer a second time.

The development preload checks APP_ID against the declared application ID. A mismatch stops startup.

Read the connection snapshot

connections(): PlatformConnections from @k2b/cloud returns the startup snapshot with appId, database.url, valkey.url, nats, appSecret, and sources. nats contains servers, namespace, replicas, ignoreClusterUpdates, and optional creds and ca contents. appSecret contains current and optional previous and next values. The matching sources fields identify env or core for each value. Before start it throws connections() used before platform start and names the startup entrypoints that initialize it. Settings reads also propagate missing startup instead of treating it as a cache outage or an unreadable stored value. Treat credentials as secrets; never return the snapshot in diagnostics.

With CLOUD_APP_KEY or CLOUD_APP_KEY_FILE, startup requests connections from Core at CLOUD_CORE_INTERNAL_ORIGIN before importing application code. Explicit valid environment values, aliases, and supported mounted files take precedence per value; Core values beat environment defaults. Without a key, startup uses environment values. Empty environment values are unset; explicit false remains false. Database and Valkey URLs must identify their services; Cloud supplies no localhost fallback. Missing NATS configuration and APP_SECRET still fail at the application's existing startup guards. Mounted NATS credentials and CA files are read once into the snapshot. Changing connections requires a process restart. Storage settings continue to take effect without a restart.

An invalid key or permanent protocol error stops startup without fallback. Network, TLS/CA, HTTP 429 and HTTP 503 errors retry within a five-minute budget with ten-second attempt deadlines. There is no HTTP listener while waiting. Keep existing infrastructure environment values until the platform-client import migration is complete; key-only production operation is not yet supported because remaining Bun-global clients still read the environment. See Runtime configuration.

Each registry instance has its own optional bootstrap metadata field: keyId (in Core mode), mode (core or env), per-value sources, keyFormat: "cloud-app-key-v1", and protocol: 1. These fields contain no credentials and do not use the shared-entry entries digests. Admin readers of listAppsDetailed() receive an instances list with bootstrap: null for older producers; missing metadata means unknown. This reports startup protocol capability, not completion of the platform-client import migration.

Bootstrap v1 transport

The initializer sends POST /_internal/apps/bootstrap directly to Core with exact UTF-8 body {} and a fresh bearer JWT per transmission. The protected header is alg: EdDSA, typ: cloud-app+jwt, kid: <keyId>. Claims bind iss = sub = appId, aud = cloud-core:<installationId>, iat, exp, jti, htm = POST, htu = /_internal/apps/bootstrap, and bdh, the unpadded base64url SHA-256 digest of the exact body bytes. A proof lives at most 30 seconds; iat may be at most two seconds ahead. jti is a nonce of 1–64 characters without NUL. Compact proofs fit Cloud's 4 KiB token budget.

Core reads the active public key from Postgres on every request, checks the signature and identities, checks the key's connection permission and Valkey budget, then consumes (keyId, jti) atomically across replicas. Admission failures leave the nonce unused. Replay evidence remains until at least exp + 5 minutes. Core's existing minute maintenance drains expired records in 100-row batches, with cooperative cancellation between batches and a per-run cap of two minutes of per-key admission capacity across all stored app keys. Remaining expired records are reported as a cleanup backlog and continued on the next run. The existing ingress budget also applies to invalid proofs. Authenticated keys have a separate budget using security.rate_limit_per_second.

A successful response is { identity: { installationId, appId, keyId }, connections: { database, valkey, nats, appSecret } }, with Cache-Control: no-store. Both sides check the schema and a 64 KiB byte limit; Core refuses an oversized delivery configuration at startup. NATS credentials and CA fields carry contents, never file paths. Responses and proofs must never be logged. The client treats a refused redirect as a permanent startup error and verifies TLS with the process's configured trust store.

Only keys on their app's own resource_bound account with resource_type = cloud.app and resource_id = app_id = iss can bootstrap. Other account kinds or bindings are treated as unknown keys. Credentials have an internal connections_allowed flag, defaulting to true; false refuses connection delivery after signature verification with HTTP 403 and code: "connections_not_permitted". There is currently no API or UI to change this flag. It does not revoke the key for other signed protocols.

HTTP 400 denotes a malformed request, 401 an invalid proof or unavailable credential, 403 a denied connection permission, identity mismatch or replay, and 413 an oversized request. HTTP 429 and 503 are retryable; 503 includes unavailable admission or storage. The client also retries network and TLS failures within its startup budget. Other HTTP statuses and invalid success responses are permanent startup errors.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.