Skip to main content

Application runtime profiles

An s-m-r-t application declares one infrastructure profile and keeps the same domain objects, workflows, generated interfaces, and approval policy when it moves between a laptop and a deployment.

import { defineConfig } from '@happyvertical/smrt-config';

export default defineConfig({
runtime: { profile: 'local' },
});

At startup, call resolveConfiguredApplicationRuntime(). It composes the loaded file with highest-priority setConfig() overrides, then returns a deterministic, immutable, machine-readable snapshot for startup, doctor, tests, and agent inspection. It reads no environment variables and contains no connection strings, paths, credentials, or secret values. Use resolveApplicationRuntime(config) only when resolving an explicit value, such as a test fixture; getConfig() retains its legacy loaded-file-only semantics. smrt app and the SvelteKit runtime resolve through resolveEffectiveApplicationRuntime(loadedConfig): no runtime block selects local, and a present value that is not a block (null, false, 0, '') fails closed.

Safe presets​

ProfileDefault compositionSupported choices
localuser-owned SQLite; single-use owner bootstrap; real default tenant; local assets and secrets; embedded jobs; loopback binding; file-snapshot backupjobs may run inline; TLS may be enabled while the bind remains loopback
self-hostedoperator PostgreSQL; OIDC; single tenant; S3-compatible assets; environment secrets; external workers; public TLS; operator backupmagic link or password auth; explicit multi-tenant mode with required context; local or S3-compatible assets; environment, local-file, or external secrets; application isolation or RLS
cloudmanaged PostgreSQL; hosted identity; required multi-tenant context with RLS; managed storage and secrets; scalable workers; public TLS; managed backupS3-compatible storage, external secret provider, or application-enforced isolation

All three presets advertise logical export and import. Provider-specific settings such as database URLs, asset roots, OIDC credentials, and secret manager identifiers remain in the owning provider's configuration. The runtime snapshot intentionally reports only safe selectors and derived capabilities.

Explicit overrides​

Profiles are presets, not environment-name conditionals. Apply supported provider changes in one place:

export default defineConfig({
runtime: {
profile: 'self-hosted',
providers: {
authentication: { provider: 'magic-link' },
tenancy: { mode: 'multi-tenant', context: 'required' },
assets: { provider: 'local-files' },
},
},
});

The resolver reports effective changes under diagnostics.overrides. Unknown fields and incompatible combinations fail before application startup with a path, reason, and recovery action. For example, local owner bootstrap cannot be combined with a public bind, and cloud mode cannot disable required tenant context or TLS.

When a runtime override selects a different profile, file-level provider overrides are discarded before the new preset resolves. This prevents a local choice such as inline jobs or local files from leaking into a cloud composition. The same reset applies when successive setConfig() calls explicitly change profile. When the profile stays the same, nested provider overrides deep-merge normally.

Cross-profile invariants​

Provider selection cannot change these application behaviors:

  • domain models and workflows;
  • generated REST and CLI surfaces;
  • generated MCP and WebMCP definitions;
  • action-effect metadata and human approval policy;
  • real authorization records;
  • the job invocation API;
  • logical export/import portability.

The resolved snapshot includes these invariants so integration tests can compare profiles directly. Implementing packages consume the contract; templates should never copy profile conditionals into application code.

Running the local profile​

@happyvertical/smrt-app-runtime implements the private local composition. The application supplies its normal idempotent migration hook; the runtime prepares the user-owned filesystem, securely acquires and tunes file-backed SQLite, creates local application-secret material, and issues the first short-lived onboarding token.

import {
initializeLocalApplicationRuntime,
} from '@happyvertical/smrt-app-runtime';

const { runtime, bootstrap, diagnostics } =
await initializeLocalApplicationRuntime({
appId: 'my-app',
sourceRoot: process.cwd(),
prepareDatabase: runApplicationMigrations,
});

The default root is the operating system's per-user application-data directory, never the source checkout, one of its ancestors, the user home itself, or the filesystem root. The application root is a dedicated directory; when an explicit root already exists, it must already be owned by the current user with mode 0700, and a failed proof leaves its mode and contents untouched. Empty roots receive an app-specific atomic ownership marker; populated roots require that marker. A pending marker safely resumes a crash between root claim and database acquisition, while failed SQL custody validation removes only a claim and directories created by the failing attempt; an inherited pending claim and database remain available for retry. The SQL boundary proves ancestor ownership/modes and macOS ACL safety before database, asset, or secret artifacts are created. Directories are mode 0700; the database and generated application-secret file are mode 0600. SQLite enables foreign keys, WAL, full synchronous durability, and a busy timeout. The local server binds to 127.0.0.1 by default, and owner bootstrap refuses a non-loopback bind. Concurrent initializers are serialized across processes by an exclusive transaction in a dedicated SQLite lock database under a private, root-keyed custody directory beside the data root (<parent>/.smrt-lease-<key>), so it also works in user-namespace sandboxes where /tmp reads as owned by the overflow uid. The legacy per-user /tmp/.smrt-<uid> lease is taken afterwards so older releases stay excluded; when /tmp's owner is unmapped it is taken lock-only (never written, private current-user lock directories and file), and it is required when the parent cannot hold a lease. In a uid-remapping namespace both the in-namespace <uid> and its /proc/self/uid_map host mapping are locked, in that order, so older releases inside the namespace and on the host both contend; the in-namespace candidate is skipped only when its root is an existing real directory owned by another user (no one in the namespace can hold it), and an unusable map fails closed. The released SQL trusted-parent boundary validates each lock directory, ancestor chain, leaf, and macOS ACLs before opening it. SQLite elects one owner atomically without deleting a lock pathname; process or worker death releases the kernel lock without PID/time leases. A read-only storage-path and marker preflight rejects obvious invalid configurations before creating a registry entry, and the complete validation runs again under the lease before application-root mutation. The lease is held through secret publication, SQLite tuning, application migrations, and bootstrap construction; contenders wait up to two minutes for completion. Initialization walks every existing storage-path component without following symbolic links, verifies canonical source-tree separation, and performs chmod through validated file descriptors. A platform without the required no-follow file and directory semantics is refused rather than initialized unsafely. After establishing its mode-0700 data root, the runtime acquires SQLite through the @happyvertical/sql node:sqlite trusted-parent custody boundary. That boundary rejects unsafe ownership, write permissions, static links, macOS ACLs, and unsupported platforms or Node runtimes before opening the database. The application secret is written and synced to a private temporary file, then published with a no-overwrite atomic hard link. Concurrent startup reuses the single complete winner; malformed existing values fail closed and interrupted temporary files are cleaned only after a valid final secret exists. It protects the custodied directory from other OS principals, not hostile code already running as the same user; that stronger boundary requires OS sandboxing and a descriptor-relative SQLite VFS.

Only an HMAC of the onboarding token is stored. The plaintext is returned once, expires within fifteen minutes, and is consumed in the same serialized database transaction that creates the real global Person, User, default Tenant, owner Role / Membership, and server-side Session. Repeated setup or startup does not duplicate those records. Tenancy can remain hidden in the local UI, but the durable default tenant is preserved for later logical migration. The ownerTenant option chooses that tenant's slug (default default) and name; a claim never writes over an existing tenant with the slug: it fails closed with owner_tenant_conflict unless ownerTenant.adoptExisting is set, and then adopts only an active root tenant that has no memberships, keeping its id. Authenticated session TTLs are whole seconds with a minimum of one second; invalid TTL configuration is rejected before any filesystem mutation.

Background work and application-defined paid capabilities are disabled by default. An explicit background opt-in exposes the regular embedded TaskRunner, preserving the same persisted enqueue and execution contract used by deployed workers. Diagnostics report these choices and bootstrap state but never read or emit application secrets, token plaintext, or token hashes.

Running self-hosted and cloud profiles​

initializeDeployedApplicationRuntime() composes the application-side seams for public deployments. Provider-specific URLs, credentials, clients, and vendor configuration stay in the provider bindings; the runtime receives only their selector, readiness boundary, and a PostgreSQL connection factory.

import {
initializeDeployedApplicationRuntime,
} from '@happyvertical/smrt-app-runtime';
import { getDatabase } from '@happyvertical/sql';

const runtime = await initializeDeployedApplicationRuntime({
profile: 'self-hosted',
providers: {
authentication: { provider: 'magic-link' },
tenancy: { mode: 'multi-tenant', context: 'required' },
assets: { provider: 'local-files' },
secrets: { provider: 'external' },
},
database: {
engine: 'postgres',
connect: () => getDatabase(postgresPrivateConfig),
close: async (db) => {
await db.close?.();
},
},
authentication: {
provider: 'magic-link',
readiness: () => magicLinkProvider.assertReady(),
},
assets: {
provider: 'local-files',
readiness: () => localAssetProvider.assertReady(),
},
secrets: {
provider: 'external',
readiness: () => externalSecrets.assertReady(),
},
prepareDatabase: runApplicationMigrations,
});

The initializer validates the profile, exact adapter identities, and a provider-owned database cleanup boundary before opening PostgreSQL. It then checks public authentication, assets, and secrets, probes the database, and runs the explicit idempotent migration hook. Any failure closes the acquired database and rejects startup. Stable failures, diagnostics, health, and readiness never include the provider's private error text or returned secret values. Provider-specific database readiness is additive: the runtime always performs a PostgreSQL-specific server-version probe before startup or readiness can succeed. If that startup cleanup fails, the redacted DeployedRuntimeCleanupError.retryCleanup() remains the explicit owner until the provider closes successfully.

The web, task-worker, and schedule-worker processes use the same initialization contract. Task and schedule processes first import the application's compiled object registration, .smrt/runtime/register.js (produced by the SvelteKit build), then call createTaskWorker() or createScheduleWorker() and start the returned ordinary s-m-r-t runner. Application code still enqueues through bg() or background(...).enqueue(); profile selection does not alter that API. Runtime shutdown drains in-flight readiness/session/worker initialization and serialized runner start/stop operations, stops every runner returned by that runtime, and then closes PostgreSQL. A caller may stop a runner earlier, but must not restart it after runtime shutdown has begun; the lifecycle-gated start() method rejects at that point. An awaited close() request from inside a tracked provider or worker operation acknowledges shutdown so the operation can unwind; external callers still await complete worker and database cleanup.

Health reports whether this runtime instance is live. Readiness rechecks PostgreSQL, authentication, assets, and secrets. It deliberately does not claim that another process or worker replica is alive; worker-fleet monitoring belongs to the operator or managed platform. Diagnostics report explicit provider selectors, tenant context/isolation, separate worker roles, and horizontal cloud topology without credentials.

Supported deployed choices​

ProfileSupported application-side choices
self-hostedOIDC or magic link; explicit single tenant with defaulted context or multi-tenant with required context; application isolation or RLS; local or S3-compatible assets; environment, local-file, or external secrets; separate task and schedule workers
cloudHosted identity; required multi-tenant context; application isolation or RLS; managed or S3-compatible object storage; managed or external secrets; horizontally scalable workers

The following combinations are refused before application startup: SQLite in a deployed profile, owner bootstrap or loopback exposure for a public deployment, missing/mismatched provider bindings, non-TLS public configuration, embedded deployed workers, cloud single/default tenancy, cloud defaulted tenant context, and operator-owned providers in the managed profile.

The composition seam does not provision PostgreSQL, TLS, buckets, identity providers, secret managers, workers, billing, or a hosted control plane. The operator applies application migrations and, when database-rls is selected, the documented s-m-r-t PostgreSQL policies. The selector validates intent; it does not mutate database roles or privileges during web startup.

SvelteKit server hooks​

SvelteKit applications consume the profiles through @happyvertical/smrt-app-runtime/sveltekit instead of copying profile conditionals, tenant selection, and diagnostics glue into each app:

// src/hooks.server.ts
import { createSmrtSvelteKitRuntime } from '@happyvertical/smrt-app-runtime/sveltekit';

export const runtime = createSmrtSvelteKitRuntime();
export const { handle, init } = runtime;

init runs the profile's fail-closed startup (local runtime, or deployed bindings, which additionally require explicit providerReadiness probes). handle waits for startup, then resolves a URL tenant candidate as selection only, loads the signed session, and enters and publishes its tenant context only for an active direct or legitimately inherited membership; any other session is treated as unauthenticated. Profiles that select database-rls isolation always run the request inside the PostgreSQL RLS transaction, even on session skip paths (as an anonymous principal), and runtime.classOptions() then hands application collections that transaction-bound database. The same entry provides mountable health, authorized diagnostics, the local owner-setup load/actions (loopback peer and loopback host re-checked per request), and a session layout summary. See the package README for options.