HomeWork / Corbelo Selected work

corbelo

Complete features,
delivered as source.

Maintained features for Apple apps, web apps and APIs. Interface, API contract and Go backend, designed together and integrated into your repository by your coding agent.

Public prelaunch. Reservations take no payment.

Corbelo for MacNative SwiftUI
Corbelo for Mac: install local tools, choose your coding agent, review the connection, and verify it before opening your project
A native companion for your project and coding tools.
My role
Product design and engineering
Scope
Apple, web, API and tooling
Delivery
Source in your own repository
Deployment
Your server, your hosting

Designed as a whole stack.

Seven starting points across native apps, the web and standalone APIs. Each feature declares which combinations it supports.

Apple
SwiftUIiOS 26 and macOS 26
Web
HTMX or ReactServer-rendered HTML or a React client
Contract
HTTP + OpenAPIShared requests, responses and errors
Backend
GoSQLite or PostgreSQL, on your server
Explore the stack decisions

The complete
feature library.

Every feature and every capability in the public catalog. Inspect a feature for its implementation, included dependencies, stack support and declared rules.

20 features 76 capabilities

Sign-in

Identity & accessAccounts people sign in to with an emailed link or a password, or with Apple, Google, GitHub or any OpenID Connect provider.6 capabilitiesAccounts and email sign-inProvider sign-in and linkingSign in with AppleSign in with GoogleSign in with GitHubOpenID Connect sign-inInspect feature

Accounts people sign in to with an emailed link or a password, or with Apple, Google, GitHub or any OpenID Connect provider.

API routes
29
data tables
7
migrations
5
declared rules
36

Feature source before its dependencies. Stack support is listed for each capability.

Accounts and email sign-inidentity.humanv0.4.12

Gives users an account they reach with a link sent to their email, with an optional password and recovery. Your server keeps every account and session.

In the product
Users enter their email and open the link it sends; the first link creates their account. Passwords are an optional fallback with their own reset, and users can sign out or delete their account.
Across the API
POST /auth/magic-link sends a link and /auth/verify exchanges it for a session. POST /auth/password/reset/request and POST /auth/password/reset recover a password, GET /me returns the signed-in account and POST /auth/logout signs out.
On the server
Stores sign-in tokens only as hashes, spends each one with a single conditional write, and creates the session in the same transaction. Emailed links always use your validated public origin.

What it guarantees

  • Each sign-in link works once and expires quickly
  • A password reset signs out every session
  • Unknown accounts take as long to check as real ones

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (1)
  • Transactional emailcommunication.email
Declared rules (5)
  • Tokens are random, hashed at rest, short-lived, and consumed by one conditional write.
  • Magic-link verification and session creation share one transaction.
  • Password reset, session revocation, and token consumption share one transaction.
  • Unknown and passwordless accounts are timing-equalized during password login.
  • Email links use the validated canonical public origin, never request authority headers.
Full capability reference
Provider sign-in and linkingidentity.external-loginv0.4.5

The shared flow every provider uses. Users sign in with an account they already have or connect one to the account they use now, and a matching email never joins two accounts by itself.

In the product
The sign-in screen lists the providers your server offers. From Account security, users connect another way to sign in or remove one.
Across the API
GET /auth/providers/available lists providers without account data. POST /auth/external/{provider}/start begins a sign-in and POST /auth/external/{provider}/complete finishes it once, with the PKCE verifier only the client holds. GET /auth/providers and DELETE /auth/providers/{identityId} manage linked accounts.
On the server
Keys each linked account by provider and immutable subject, keeps authorization state hashed and single-use, discards provider tokens, and runs your admission rules inside the sign-in transaction.

What it guarantees

  • A matching email never links accounts by itself
  • Provider access and refresh tokens are never stored
  • A refused sign-in writes nothing

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Accounts and email sign-inidentity.human
  • Sessions and security historyidentity.security-center
  • Product settingsproduct.settings
Declared rules (12)
  • External subjects are keyed by provider plus immutable subject; email equality never links an existing account.
  • Provider access and refresh tokens are transient and are never persisted.
  • Authorization state is random, hashed at rest, single-use, short-lived, provider-bound, and paired with PKCE and OIDC nonce verification.
  • Linking and unlinking require recent local authentication, and a link completes only for the user and in the session that began it.
  • A provider that has not verified the email it returns never creates an account; the refusal names the reason and writes nothing.
  • The signed-out provider list names providers only, never account data, and an app callback forwards only the state and the code or the provider's error to the one configured app scheme.
  • Provider callbacks use the configured canonical origin; arbitrary return URLs, redirect following, and dynamic callback origins are rejected.
  • A verified provider email may create a new local user; an existing local email requires explicit linking from Account security.
  • Admission policies run inside the sign-in transaction before any account, link or session is written; a refusal writes nothing.
  • An API redirect sign-in completes once, with the state it began and the PKCE verifier only its client holds.
  • The host remains authoritative for users, sessions, security evidence, and secret material.
  • A provider that posts its callback keeps a Secure, SameSite=None, __Host- flow cookie; it relaxes to a Lax cookie without Secure only while APP_ENV is development and PUBLIC_BASE_URL is a plain-HTTP loopback origin, and every other environment or origin keeps the Secure cookie.
Full capability reference
Sign in with Appleidentity.external-login.applev0.4.5

Adds Sign in with Apple. Your server signs short-lived client assertions with its own P-256 key and accepts an Apple ID token only when every claim matches.

In the product
Users tap Sign in with Apple and approve it with their Apple Account. The app passes the identity token Apple returns to your server, which checks it before any session exists.
Across the API
POST /auth/apple signs in with an Apple identity token, POST /auth/apple/link connects Apple to the signed-in account, and POST /auth/apple/notifications receives Apple’s account events.
On the server
Requires Apple’s exact issuer, your product’s audience, an RS256 signature, a valid lifetime and the transaction’s nonce. Partial or malformed key configuration stops the server at startup.

What it guarantees

  • The private key never leaves your server
  • Every ID token must carry its transaction’s nonce
  • The local stand-in runs only in development

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Provider sign-in and linkingidentity.external-login
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (4)
  • The Apple private key remains host-owned and only signs bounded short-lived client assertions in memory.
  • Apple ID tokens require exact issuer, product audience, RS256 signature, valid lifetime, and transaction nonce.
  • Partial configuration or malformed non-P-256 key material fails at process startup.
  • The APPLE_SIGN_IN_LOOPBACK_* override replaces every Apple URL and the issuer with loopback ones, is refused unless APP_ENV is development, and readiness never probes it.
Full capability reference
Sign in with Googleidentity.external-login.googlev0.4.5

Adds Sign in with Google through OpenID Connect. Your server accepts a Google ID token only with the exact issuer, your audience, a valid signature and lifetime, the transaction’s nonce and a verified email.

In the product
Users choose Sign in with Google. The app passes the ID token Google returns to your server, which checks it before any session exists.
Across the API
POST /auth/google signs in with a Google ID token and POST /auth/google/link connects Google to the signed-in account.
On the server
Requires an allowed algorithm, Google’s exact issuer, your audience, a valid signature and lifetime, and the transaction’s nonce. A token from the app must name one of your client IDs and carry the SHA-256 of the nonce the app kept.

What it guarantees

  • Client credentials stay with your server
  • No Google token is stored
  • Partial configuration stops the server at startup

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Provider sign-in and linkingidentity.external-login
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (5)
  • Google ID tokens require an allowed algorithm, exact issuer, product audience, valid lifetime, signature, and transaction nonce.
  • Client credentials stay host-owned and provider tokens are discarded after claim verification.
  • Partial provider configuration fails at process startup.
  • A native ID token from the app must name exactly one of the product's client ids as its audience, an authorized party among them, and the hex SHA-256 of the nonce the app kept as its nonce claim; no Google token is stored.
  • The GOOGLE_OIDC_LOOPBACK_* override replaces every Google URL and the issuer with loopback ones, is refused unless APP_ENV is development, and readiness never probes it.
Full capability reference
Sign in with GitHubidentity.external-login.githubv0.2.3

Adds Sign in with GitHub. The account’s numeric ID is the identity, and only an email GitHub has verified can create an account.

In the product
The app opens GitHub’s authorization page in an ephemeral browser session, asking for user:email alone. Your server finishes the sign-in.
Across the API
Adds no routes of its own. It signs in through the provider flow at /auth/external/github/start, and only the server redeems the code.
On the server
Holds the client secret, redeems the code, reads GitHub’s verified-email endpoint and drops the access token once the profile is loaded.

What it guarantees

  • Login names and emails are never identity keys
  • Only a verified email creates an account
  • A matching address links only from the account holder’s session

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Provider sign-in and linkingidentity.external-login
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (6)
  • The immutable numeric GitHub account ID is the subject; login name and email are never identity keys.
  • Only an email confirmed by GitHub's verified-email endpoint may create a local account.
  • The transient GitHub access token is held only long enough to load the profile and is never persisted.
  • On the API and Apple profiles GitHub signs in through identity.external-login's redirect flow; the server alone redeems the code, holds the client secret and reads GitHub's API.
  • A GitHub address matching an existing account is never linked by sign-in; the account holder connects GitHub from a recent session.
  • The Apple button opens only GitHub's authorization page for the flow it started, asking for user:email alone, in an ephemeral browser session.
Full capability reference
OpenID Connect sign-inidentity.external-login.oidcv0.3.2

Connects any OpenID Connect provider, such as the one your organization runs. You configure each endpoint explicitly, and only allowed signatures are accepted.

In the product
Users choose Sign in with your organization, continue on their provider’s page and come back through the shared provider flow.
Across the API
Adds no routes of its own. It signs in through the provider flow at /auth/external/oidc/start.
On the server
Uses only the issuer, authorization, token and key endpoints you configure, never runtime discovery. Accepts HTTPS endpoints, the exact issuer and audience, RS256 or ES256 signatures, a valid lifetime and the transaction’s nonce.

What it guarantees

  • Endpoints are explicit configuration, never discovered
  • Plain-HTTP endpoints are refused in production
  • The development provider answers only on loopback

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Provider sign-in and linkingidentity.external-login
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (4)
  • Issuer, authorization, token, and JWKS endpoints are explicit host configuration; runtime discovery and arbitrary callback-controlled endpoints are forbidden.
  • Only HTTPS endpoints, exact issuer and audience, allowed RS256 or ES256 signatures, valid lifetimes, and transaction nonces are accepted; plain-HTTP loopback endpoints need OIDC_ALLOW_LOOPBACK, which production refuses.
  • Partial provider configuration fails at startup and access or refresh tokens are never persisted.
  • The development provider, cmd/oidc-dev-idp, listens on and answers only loopback addresses, makes its signing key and client secret for each run, and is never part of a production deployment.
Full capability reference
Explore Sign-in

Account security

Identity & accessPasskeys, authenticator codes and recovery codes, the signed-in devices people can revoke, and abuse checks with rate limits behind them.5 capabilitiesPasskeys and two-step sign-inSessions and security historyHuman and device challengesExplainable risk decisionsDurable rate limitsInspect feature

Passkeys, authenticator codes and recovery codes, the signed-in devices people can revoke, and abuse checks with rate limits behind them.

API routes
30
data tables
12
migrations
9
declared rules
42

Feature source before its dependencies. Stack support is listed for each capability.

Passkeys and two-step sign-inidentity.assurancev0.3.6

Adds passkeys, authenticator codes and one-time recovery codes to the sign-in your product already has. Password, magic link, external and enterprise sign-in all pass one check before any session exists.

In the product
Users add a passkey with Face ID or Touch ID, scan an authenticator QR code, keep their recovery codes, and answer a code prompt when they sign in.
Across the API
A sign-in that needs a second factor answers 403 identity.assurance.mfa_required with a sealed, single-use ticket. POST /auth/mfa finishes it.
On the server
Checks each passkey’s origin, signature and counter, encrypts authenticator seeds with a host-owned key, hashes recovery codes, and creates the session only after the code is accepted.

What it guarantees

  • No session while a second factor is pending
  • Recovery codes are shown once, hashed and spent once
  • Security changes need a sign-in from the last ten minutes

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (4)
  • Sessions and security historyidentity.security-center
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • Secret referencessecurity.secret-references
Declared rules (11)
  • Passkey ceremonies validate exact origin, RP ID hash, challenge, type, user presence, user verification, ES256 signature, and monotonic counters.
  • Challenges are random, hashed, single-use, provider-bound, and expire after five minutes.
  • Every interactive login crosses the host completion seam before any session is kept; enabled TOTP applies to every method except a user-verified passkey.
  • TOTP seeds are encrypted with a host-owned key and the exact accepted counter is persisted so adjacent-window codes cannot be replayed.
  • Recovery codes are random, shown once, hashed at rest, and consumed conditionally.
  • A passkey authenticates into the existing human session authority rather than creating a parallel session system.
  • A sign-in held for a second factor has no session on any stack: the first factor's session is deleted, the browser's pending cookie and the API's 403 identity.assurance.mfa_required ticket are sealed and name only a single-use challenge, and the session is created only after a code is accepted; a wrong code ends the held sign-in, and a spent ticket changes no session.
  • Every assurance change requires a sign-in from the last ten minutes, and assurance operations admit only the account's own session, never an API key or agent.
  • Five wrong second-factor codes for one account in fifteen minutes lock its second factor until the window closes, answered 429 with Retry-After; the count is one database row per account, so every server process and restart shares the lock, and the API's passkey and second-factor throttles are separate per-peer, per-process buckets that only shed load.
  • The authenticator setup and enable operations disclose a secret: session-only fixed actors, no Idempotency-Key, never on the flow surface; the enrollment code's step is spent.
  • Passkey names are at most 80 characters, never cut, and are checked before the registration challenge is spent.
Full capability reference
Sessions and security historyidentity.security-centerv0.3.5

Shows users every device signed in to their account and a history of security events. They can sign out one device or every other device, and your server’s session table stays the only authority.

In the product
Lists signed-in devices with coarse labels and when each was last used, marks this device, confirms before signing one out, and shows recent security events.
Across the API
GET /auth/sessions and GET /auth/security-events return the account’s devices and history. DELETE /auth/sessions/{sessionId} signs one device out. The web adapter adds the same controls under /settings/security.
On the server
Ends live authority in the same transaction as the history entry, keeps only token hashes, and never stores raw IP addresses or full user-agent strings.

What it guarantees

  • A revoked session stays revoked
  • Signing out every other device keeps this one
  • History is append-only and keeps safe summaries

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Accounts and email sign-inidentity.human
  • Product settingsproduct.settings
Declared rules (7)
  • The base session table remains the sole authentication authority; security history can never restore a revoked session.
  • Only token hashes link security descriptors to live sessions, and hashes are never rendered or logged.
  • Device labels are coarse and bounded; raw IP addresses and full user-agent strings are not retained.
  • Revocation is tenant-scoped and deletes live authority in the same transaction as security history.
  • The current session is explicit, and revoking all others preserves it.
  • Security events are append-only and contain safe summaries rather than credentials or raw request metadata.
  • Last-seen writes are bounded and coalesced without weakening immediate revocation.
Full capability reference
Human and device challengessecurity.challengev0.3.3

Asks for proof that a person or a genuine app is behind a sensitive action. Browsers prove it with Cloudflare Turnstile and Apple apps with App Attest, and each proof is spent on one action.

In the product
When a guarded action needs proof, the client completes a Turnstile check in the browser or an App Attest assertion on the device, then sends the action again.
Across the API
A refused request answers challenge.required. POST /challenge/proofs trades a verified proof for one clearance, and the retry uses a new Idempotency-Key.
On the server
Validates Turnstile through Siteverify with your secret, verifies App Attest against Apple’s pinned root without a network call, and binds each proof to one resource that it can clear once.

What it guarantees

  • One proof clears one resource, once
  • Production refuses the development provider and keys
  • Raw tokens, payloads and addresses are never stored

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. App Attest is tested against synthetic attestations. An end-to-end run with a real Apple Developer team is still to come.

Included dependencies (5)
  • Explainable risk decisionssecurity.risk
  • Durable rate limitssecurity.rate-limit
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • Secret referencessecurity.secret-references
Declared rules (10)
  • Products register challenge purposes with expected provider action, proof lifetime, and fail-open or fail-closed behavior.
  • Production rejects the deterministic local provider and development App Attest keys.
  • Turnstile tokens are validated through Siteverify with a host-owned secret and bounded client timeout; the widget's cdata carries the resource binding.
  • App Attest proofs are verified in the plugin against the pinned Apple root without a network call; the assertion signs client data naming the purpose, action, resource binding and issue time.
  • A proof is bound to one resource through ResourceBinding(purpose, resource) and refused when bound elsewhere, stale or issued in the future.
  • Proofs bind provider, purpose, and token; a conditional unique insert permits exactly one use whoever presents the token.
  • A verified proof grants the caller one clearance for one purpose and resource; a guarded intent spends it once, inside its own transaction when it declares one.
  • A request refused with challenge.required is finished; the client completes the challenge and sends the intent again as a new request with a new Idempotency-Key.
  • Raw proof tokens, provider payloads, secrets, and network addresses are never stored.
  • Provider errors and invalid proofs produce safe local reason codes and expiring evidence, and a scheduled purge removes expired evidence in bounded batches.
Full capability reference
Explainable risk decisionssecurity.riskv0.3.3

Turns product signals and provider scores into a decision with a named reason. Your rules stay the authority; a provider’s score is only an input.

In the product
Your app can read the active policies and the signed-in account’s own decision evidence, and dry-run a policy you mark previewable.
Across the API
GET /risk lists policies and signals. POST /risk/evaluate dry-runs a previewable policy against the signals you send and records nothing. Enforcement stays inside server-owned operations, because a client cannot assert a trusted product signal.
On the server
Evaluates versioned rules in a fixed order with a fail-closed default, stores only a signal digest and a safe reason code, and purges expired evidence every hour.

What it guarantees

  • Unknown, stale or out-of-range signals fail closed
  • Provider scores are inputs, never authority
  • Evaluation is separate from enforcement

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (8)
  • Products register immutable versioned policies, typed signals, deterministic ordered rules, a fail-closed default, and evidence expiry.
  • Provider signals are explicitly labeled untrusted inputs and never become authority by themselves.
  • Only a canonical signal digest, safe reason code, rule identity, and bounded resource references are stored.
  • Unknown, duplicate, mislabeled, stale, or out-of-range signals fail closed.
  • Evaluation remains separate from enforcement so every protected operation chooses how to satisfy a disposition.
  • Expired evidence is purged automatically in bounded batches by a worker every composition registers, and no raw provider payload is retained.
  • A dry run evaluates only a policy the product marked previewable, requires dry_run true, and records no evidence.
  • Risk evaluates with its own versioned policies and does not depend on security.policy.
Full capability reference
Durable rate limitssecurity.rate-limitv0.2.2

Gives each sensitive operation a named limit with its own window, cost and failure posture. Keys come from trusted request context, so callers never choose what they are counted as.

In the product
No screens of its own. It sits behind actions users already take, including the challenges above.
Across the API
Adds no routes. A refused action returns bounded remaining and retry information, and never reveals the key it was counted under.
On the server
Every adapter, on SQLite and PostgreSQL alike, decides with one conditional upsert so concurrent requests cannot overspend, stores only key digests, and keeps one current window per operation and key.

What it guarantees

  • Network limits always include a resource
  • Key dimensions never substitute for each other
  • A store outage follows the registered posture

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (1)
  • Accounts and email sign-inidentity.human
Declared rules (6)
  • Every limit is a registered named operation with an explicit window, cost, trusted key strategy, and failure posture.
  • Network limits always include a resource dimension; global IP-only policies are not supported.
  • Only digests of derived keys are persisted.
  • A conditional SQLite upsert serializes the final allowed unit so concurrency cannot overspend.
  • Tenant, actor, network, and resource dimensions cannot be substituted for one another.
  • Store unavailability follows the registered fail-open or fail-closed posture and returns a safe reason code.
Full capability reference
Explore Account security

Enterprise sign-in

Identity & accessCompany sign-in through the customer's own identity provider for a verified domain, with SCIM provisioning of people and groups.1 capabilityOrganization sign-in and SCIMInspect feature

Company sign-in through the customer's own identity provider for a verified domain, with SCIM provisioning of people and groups.

API routes
26
data tables
4
migrations
3
declared rules
14

Feature source before its dependencies. Stack support is listed for each capability.

Organization sign-in and SCIMidentity.enterprisev0.3.2

Connects a team’s verified domain to its organization’s OpenID Connect provider. People from that domain sign in there and join the team, and the organization’s directory adds and removes them over SCIM 2.0.

In the product
Users enter their work email and the sign-in screen offers their organization’s provider by name. In a browser, the organization sign-in page sends them to it with their address filled in. Owners set up the connection through the API; the supplied screens cover signing in.
Across the API
POST /auth/enterprise/discover finds the provider for a work email. PUT /teams/{teamId}/enterprise saves a connection, POST /teams/{teamId}/enterprise/token rotates its SCIM token and POST /teams/{teamId}/enterprise/disable turns it off. The directory drives /scim/v2/Users and /scim/v2/Groups.
On the server
Checks the verified domain and its holder again at every discovery, sign-in and SCIM call, runs the sign-in policy inside each sign-in, and reports membership changes through team hooks. SCIM requests are bounded and stay outside the product API contract.

What it guarantees

  • Local accounts and sessions stay authoritative
  • A deactivated person is signed out everywhere
  • SCIM tokens are shown once and stored as hashes

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Federation uses OpenID Connect. SAML providers and sign-in started from the provider’s own portal are not supported yet. Single sign-on proves identity and can establish team membership; it does not itself purchase or assign paid developer access.

Included dependencies (5)
  • Audit trailaudit.trail
  • Teams and rolescollaboration.teams
  • Provider sign-in and linkingidentity.external-login
  • OpenID Connect sign-inidentity.external-login.oidc
  • Custom domainsnetwork.domains
Declared rules (14)
  • Local users and sessions remain authoritative while the configured external provider supplies identity evidence.
  • A connection governs only a domain its team holds: the account holding the domain's verified network.domain-verification registration must save the connection itself, which records it as the connection's holder, and another owner may then change only that connection's settings; every other save is refused with the same detail. The proof that the recorded holder is still the verified holder and still an owner of the team is checked again at every discovery, sign-in, link, enforcement and SCIM grant, so a removed or moved registration or a holder who stops owning the team pauses the connection instead of trusting it, and making a new holder a team owner does not resume it.
  • A domain has one connection: a team whose holder saves a connection for a domain releases another team's connection that no longer proves it, reporting its groups to the group hooks as deleted and auditing the release on both teams, while a connection that still proves the domain keeps it and the save conflicts.
  • Every sign-in method crosses the enterprise policy before a session is returned: a deactivated account, a disabled connection, a lapsed domain on the connection's provider, and a non-SSO sign-in for a governed person under enforcement are refused with a stable code and no account is created.
  • The connection's provider is scoped to the connection's verified domain, not reserved for it: people from other domains sign in through the same provider without joining the organization.
  • Pre-provisioned account linking is allowed only for a matching verified email and an active connection-scoped trust policy.
  • A lapsed connection can still deactivate users, remove group members, rename and delete groups, update an active user's profile, rotate its token and be disabled, so an organization can always remove access; it never adds anyone to a group, reactivates anyone or restores a membership.
  • A SCIM user must belong to the connection's email domain; deactivation removes enterprise membership through the team mutation hooks and revokes every session without deleting the human account.
  • A deactivated link never signs in and just-in-time provisioning never reactivates it; only the directory reactivates, and reactivation reuses the same account.
  • The product decides who a connection governs (population), which settings it accepts (connection policy) and what group membership means (group hook); membership changes reach team mutation hooks as provisioned, linked and deprovisioned.
  • SCIM bearer tokens are random, shown once, scoped to one connection, and stored only as hashes; saving an active connection keeps its token.
  • Only team owners with recent authentication can create, change, rotate, or disable enterprise connections.
  • SCIM request bodies, filters, pagination, groups, and patch operations are explicitly bounded; SCIM stays outside the product API contract.
  • Erasing the account that created a connection keeps the connection with its team and clears its creator; the audit trail keeps who saved it.
Full capability reference
Explore Enterprise sign-in

Teams and permissions

Data & collaborationTeams with roles, invitations by email, deny-by-default permissions with custom roles, and an audit trail that verifies offline.6 capabilitiesTeams and rolesTeam invitationsNamed developer seatsPermissions and custom rolesAccess policiesAudit trailInspect feature

Teams with roles, invitations by email, deny-by-default permissions with custom roles, and an audit trail that verifies offline.

API routes
29
data tables
13
migrations
13
declared rules
51

Feature source before its dependencies. Stack support is listed for each capability.

Teams and rolescollaboration.teamsv0.6.6

Teams with three roles. Owners run the team, admins manage its members and members take part. Every team always keeps at least one owner.

In the product
Users create a team, see who is in it and what role each person holds. Owners change roles and remove people, admins remove members, and anyone but the last owner can leave in their own session.
Across the API
POST /teams creates a team and GET /teams/{teamId}/members lists its members. PUT /teams/{teamId}/members/{memberId} changes a role, DELETE removes someone and POST /teams/{teamId}/leave leaves. Deleting a team takes its owner typing its slug.
On the server
Rechecks the caller’s role inside each write and encodes it into the role-change and removal queries, so the last owner can never be removed or downgraded, even through direct SQL or account deletion.

What it guarantees

  • Every team keeps at least one owner
  • Admins never create, remove or promote owners
  • A retried create answers the same team

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (11)
  • Every team is created with one owner in the same transaction.
  • The final owner cannot be removed or downgraded, including through direct SQL or account deletion.
  • Admins may manage members but cannot create, remove, or promote owners.
  • Every public tenant-scoped store method takes the acting user ID first.
  • Role authorization is rechecked inside the write transaction and encoded into role-change and removal queries.
  • As approval reviewers, a team role resolves only to people who share a team with the owner, looked up again when each reviewer answers.
  • Only an owner deletes a team, in their own session, after typing its slug; memberships and team-keyed records go in the same transaction.
  • A retried create answers the team its first attempt made for the same owner and name, never a second team.
  • Anyone leaves a team in their own session, whatever their role, except its last owner; a key or an assistant never leaves for them, and no product may let one.
  • On the API stacks every team operation names its callers: reading teams and making one admit the person's session, keys and assistants; adding, re-roling and removing a member admit the person's own session unless a product widens them; leaving and deleting a team admit the session alone and are fixed, so no product may admit another kind. A flow runs none of them, and every refusal by kind is 403 actor_not_allowed.
  • With audit.trail 0.5.0 or later composed, every team mutation, invitations' included, joins the team's audit trail in the mutation's transaction, a deletion joins the deleting owner's, and a team's current members read its trail.
Full capability reference
Team invitationscollaboration.invitationsv0.6.6

Owners and admins invite people by email. The invitation opens in the app or a browser, and only the person signed in with the invited, verified address can join or decline it.

In the product
Managers send, resend and revoke invitations and see when each one expires. The invited person reviews the team and role, then joins or declines. A browser shows the same choice to someone without the app.
Across the API
POST /teams/{teamId}/invitations invites, .../resend sends a fresh link and DELETE .../{invitationId} revokes. The recipient answers through POST /invitations/review, /invitations/accept and /invitations/decline.
On the server
Stores each invitation and the intent to mail it in one transaction and sends mail only after it commits. Acceptance rechecks the verified address, expiry, revocation, role and existing membership in a single write.

What it guarantees

  • Tokens are single-use and stored only as hashes
  • Resending ends the previous link
  • Only owners invite admins

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (6)
  • Teams and rolescollaboration.teams
  • Audit trailaudit.trail
  • Agent surfaceapi.agent-surface
  • Transactional emailcommunication.email
  • Accounts and email sign-inidentity.human
  • Product settingsproduct.settings
Declared rules (9)
  • Invitation tokens contain 256 random bits, are stored only as SHA-256 hashes, expire, and are single-use.
  • Acceptance atomically rechecks the verified account email, expiry, revocation, role, the product's offered roles, existing membership, and durable token authority.
  • Resend rotates the token; stale delivery completion cannot update the new delivery state.
  • An invitation and the intent to mail it commit in one transaction; mail is sent only after commit, from a lease on the invitation's own row, and every delivery attempt mints a fresh link whose hash alone is stored.
  • Only owners can invite admins; ownership transfer is never performed through an invitation.
  • Every management operation is tenant-scoped and role-authorized inside the write transaction.
  • Each operation names its callers: listing and revoking admit a manager's session, key or assistant; inviting and resending, which grant membership, only the manager's own session unless the product widens them; reviewing, accepting and declining only the invited person's own session, fixed so a product may narrow them and never widen them. No operation admits a flow, and a refused kind answers 403 actor_not_allowed.
  • Invitation state changes emit team mutation hooks, through which collaboration.teams' audit bridge records them on the team's trail in the same transaction when audit.trail is composed.
  • Only the signed-in recipient with the invited verified address declines; a decline revokes the invitation in the same write and never changes after.
Full capability reference
Named developer seatscommerce.seatsv0.1.1

Named developer assignments backed by explicit organization funding, separate from owner, admin and member roles.

In the product
The supplied SwiftUI section shows purchased, assigned and available seats, paid-through time and each developer’s current or retained historical access. Authorized managers review assignment, removal and reassignment.
Across the API
GET /teams/{teamId}/seats reads a product’s capacity and revision. Assign, unassign and reassign operations bind the exact reviewed revision and a durable mutation identity; direct calls recheck the caller’s role.
On the server
Applies verified capacity evidence and serializes assignment changes with team membership. Retries retain the business identity. Changing membership or funding revokes current access while preserving eligible historical rights.

What it guarantees

  • A team role is not a paid developer seat
  • Stale revisions refuse without overwriting another change
  • Personal purchases remain independent

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Seat allocation does not create a subscription. Your product supplies its licensed product identity, verified funding evidence, checkout and renewal policy. The native section and API are supplied; adapt the browser presentation to your product.

Included dependencies (5)
  • Teams and rolescollaboration.teams
  • Team invitationscollaboration.invitations
  • Agent surfaceapi.agent-surface
  • Feature access by plancommerce.entitlements
  • License keyscommerce.license-keys
Declared rules (5)
  • Membership and roles do not grant paid developer access.
  • Only verified internal commerce evidence funds slots.
  • Capacity, assignment, replay, audit and credential changes commit together.
  • Organization access requires live membership and assignment; Personal rights remain independent.
  • Online revocation is immediate after commit; offline leases have an explicit bound.
Full capability reference
Permissions and custom rolessecurity.authorizationv0.3.2

Fine-grained permissions behind each role. Your product declares every permission and role in source; a team’s own roles may only hold permissions you registered.

In the product
Owners pick each person’s role from the roles the team offers. The app offers only the actions a person’s role allows, and the server decides either way.
Across the API
GET /teams/{teamId}/roles lists the roles a team offers and the permissions they grant. PUT /teams/{teamId}/role-assignments/{memberId} assigns one and DELETE ends it.
On the server
Denies every unknown permission, role, resource kind and missing membership. Decisions carry stable reason codes, never sensitive attributes, and every change is authorized again where it happens.

What it guarantees

  • Unknown means denied
  • Nobody hands out a permission they lack
  • An owner’s role is never reassigned

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Teams and rolescollaboration.teams
  • Access policiessecurity.policy
  • Accounts and email sign-inidentity.human
Declared rules (7)
  • Every unknown permission, role, resource kind, condition error, and missing membership denies.
  • Product source owns the immutable registry and typed condition evaluators.
  • Tenant roles may only contain registered permissions and retain a registry digest.
  • Discovery never grants authority; domain mutations authorize again.
  • Decision diagnostics expose stable reason codes, never sensitive attributes.
  • A tenant role never takes a registered role's identifier.
  • Only the person signed in reads or changes role assignments; an assignment never hands out a permission the assigner lacks, never changes their own role or an owner's, leaves an admin's to owners, and ends with its membership, its team and its holder's account.
Full capability reference
Access policiessecurity.policyv0.3.3

Versioned policies your product registers in source, each with test vectors that must pass before it serves traffic. A policy that allows by default is rejected.

In the product
Users see the outcome where a policy applies: an action held for review or refused with a reason. The supplied screens do not edit policies; they change with your source.
Across the API
GET /policy/decisions reads an account’s newest decisions and the evidence behind them. A flow run is refused by kind and never reads why a policy held it.
On the server
Evaluates rules by priority in a fixed order and writes append-only evidence with the exact policy digest, in the same transaction as the operation it governs.

What it guarantees

  • Defaults are deny, review or hold
  • Evidence commits with the operation
  • Inputs never carry credentials

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (8)
  • Product source registers immutable versioned policies and embedded test vectors before serving traffic.
  • Policy defaults are deny, review, approval, hold, or fail-closed; an allow-by-default policy is rejected.
  • Rules evaluate deterministically by descending priority and stable rule identity.
  • Every accepted evaluation writes append-only tenant-scoped evidence with the exact policy digest.
  • EvaluateTx writes decision evidence in the caller's transaction, so evidence commits only with the operation it governs.
  • Policy inputs contain bounded references and attributes, never credentials, raw commands, or secret material.
  • Policy decisions advise product enforcement; routes and domain services remain authoritative.
  • security.policy.decisions.list declares the account's session, keys and assistants as its callers, so a flow run is refused by kind with actor_not_allowed and never reads why a policy held it.
Full capability reference
Audit trailaudit.trailv0.6.5

An append-only record of who changed what, kept per owner and per team. Each record is chained to the one before by hash, so anyone holding a past head can verify the trail offline.

In the product
Owners read the latest security-sensitive changes on the Activity page and export them. With a signing key, an export carries a signed checkpoint.
Across the API
GET /activity lists recent changes and GET /activity/export exports a trail: the account’s own, or a team’s its current members read. GET /activity/verify checks a range, and GET /activity/keys publishes the signing keys to pin.
On the server
Records each team change in the same transaction as the change. Records cannot be updated or deleted, and members appear under a pseudonym that erasing their account makes unreadable while every hash still verifies.

What it guarantees

  • Records never change after they are written
  • Erased users stay erased
  • Checkpoints verify against a pinned key

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. A chain proves nothing was changed, removed or reordered since a head someone kept elsewhere. It cannot prove the honesty of an operator who controls the database and rewrites every hash.

Included dependencies (1)
  • Agent surfaceapi.agent-surface
Declared rules (11)
  • Every event belongs to one host-resolved owner and owner identity is the first argument to every scoped store method.
  • Every event records a structured actor, action, resource, occurrence time, and JSON object metadata.
  • Audit events are immutable after insertion; the adapter exposes no update or delete API and SQLite triggers reject either mutation.
  • The store generates event IDs and recording timestamps rather than accepting them from an HTTP client.
  • Each owner's events form a hash chain (ADR 0042): consecutive sequence numbers from 1, each hash the SHA-256 of the previous hash and the event's RFC 8785 canonical fields, which commit to the metadata's digest rather than the metadata; the owner's head row serializes appends on every database.
  • A trail stores each actor under a pseudonym of its own (ADR 0042 as amended): ap_ and 128 bits of an HMAC-SHA256 under a random secret in the trail's key for that actor. The chain hashes the pseudonym, reads resolve it through the key, and erasing the actor deletes their keys on every trail, so the pseudonym names nobody while every hash still verifies.
  • Domain state and its resulting audit event can commit through one store-owned SQL transaction.
  • Metadata is size-bounded and must not contain secrets or unnecessary sensitive values.
  • Audit recording errors are returned to the caller and are never silently discarded.
  • A chain proves that no record was changed, removed or reordered since a head hash someone kept elsewhere; it does not prove the honesty of an operator who controls the database and rewrites every hash.
  • With a signing key, an export's head is signed with Ed25519 (ADR 0048), and a checkpoint verifies only against a key the verifier pinned out of band, never one a bundle carries.
Full capability reference
Explore Teams and permissions

Sync and realtime

Data & collaborationOffline records on Apple devices that sync with explicit conflicts, and live updates between people working together.2 capabilitiesOffline syncLive updatesInspect feature

Offline records on Apple devices that sync with explicit conflicts, and live updates between people working together.

API routes
14
data tables
8
migrations
3
declared rules
28

Feature source before its dependencies. Stack support is listed for each capability.

Offline syncdata.syncv0.8.6

Records each account keeps on its devices, a change feed ordered by one sequence, and an offline queue whose every mutation is applied once. A conflict comes back with the server’s copy; it is never merged for you.

In the product
Users edit notes without a connection, see how many changes wait to sync and sync when they choose. When another device changed the same note, the notebook keeps both versions as separate notes.
Across the API
POST /sync/push applies up to 200 mutations in order, once per mutation ID. GET /sync/changes?since= reads the account’s changes after a cursor and GET /sync/records/{collection}/{recordId} reads one record. The same routes under /sync/teams/{teamId} serve a team’s records to its current members.
On the server
Applies a mutation only against the version the device last saw. On the account routes a conflict answers with the server’s record, and on team routes the client reads it again with membership checked. The server never merges bodies. Team routes check current membership inside the transaction, and sync stays off until the product says who may use it.

What it guarantees

  • A replayed mutation returns its recorded outcome
  • The change feed never rewrites history
  • Sync fails closed until the product allows it

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite

Sync keeps a device's local copy current, offline included. A web or React product reads the server directly and keeps no local copy to sync.

Known limit. Records are bounded JSON objects, not files: a record may name a stored file’s key. Sync does not push changes to devices; each one pulls from its cursor.

Included dependencies (3)
  • Agent surfaceapi.agent-surface
  • Public pagescontent.public-pages
  • Accounts and email sign-inidentity.human
Declared rules (12)
  • Account routes isolate records by account. Team routes check current membership inside the transaction; IDs and cursors never grant authority. Team data has no public share path.
  • A mutation applies only against the version the client last saw; a conflict returns the server's record and the client merges; the server never merges bodies.
  • A mutation id is applied once; a replay returns the recorded outcome.
  • The change feed is ordered by a per-account sequence and never rewrites history.
  • A share publishes a snapshot of one record through content.public-pages; revoking the share unpublishes the page; the record's later changes refresh the page while shared.
  • Bodies are bounded JSON objects; the server stores them as sent and inspects only string, number and boolean fields for the share page.
  • Team mutation receipts bind actor, team, device and exact canonical intent, including conflict/rejected results. Removal is checked before every replay.
  • Team cursors bind actor and team; native caches and queues bind origin, account and team. Account scope remains backward compatible.
  • Sync fails closed: until the product configures an authorizer no record is written, read or published, and only listing and revoking shares stay open. A configured authorizer is asked about every action, listing and revoking included, with a SyncScope naming the account and, for team operations, the team, which reaches it only after current membership is checked.
  • Every route is a data.sync operation registry declaration: the caller is the operation's actor, a push, a team push or a share sent again under the same Idempotency-Key is told the first answer, and the REST surface reads at most 1 MiB of a request body.
  • Each operation declares its callers: sync.shares.create admits the person's own session alone, because choosing what becomes public is the owner's act, and every other operation admits the person's session and a key or an assistant with its sync scope, so a push by any of them refreshes a live share and any of them may end one. None is fixed, a product that widens publishing owns that exposure, and an automation flow or a visitor is refused.
  • Deleting a team deletes its sync records, changes and mutation receipts in the same transaction.
Full capability reference
Live updatescollaboration.realtimev0.5.2

Typed topics scoped to a tenant before a resource, delivered as server-sent events on every starting point, with resumable cursors, one coalescing publisher and a Swift stream client.

In the product
A change made in one place appears in the others without a refresh, because open screens hear about it and read it again. A dropped stream reconnects on its own, with growing, jittered waits.
Across the API
GET /realtime/{topic}/{resource} streams text/event-stream to a signed-in caller and resumes with Last-Event-ID. The web and React starting points add a WebSocket and a presence list at /realtime/{topic}/{resource}/presence.
On the server
Publishes a minimal notice only after the change commits, through one Publisher that sends at most one event per topic, resource and type each second by default. Sessions and topic access are checked again while a stream stays open, and a slow subscriber is disconnected rather than blocking anyone.

What it guarantees

  • Every stream is scoped to a tenant first
  • Events are signals to refetch, not data
  • A restored database tells clients to resync

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. The API starting points fan out within one process, so run a single instance. The web and React starting points can fan out through Redis, whose URL stays with the host.

Included dependencies (5)
  • Accounts and email sign-inidentity.human
  • Domain eventsintegration.events
  • Provider readinessprovider.readiness
  • Permissions and custom rolessecurity.authorization
  • Secret referencessecurity.secret-references
Declared rules (16)
  • Every stream and presence key is tenant-scoped before topic and resource.
  • Topic descriptors declare event types, permissions, payload bounds, replay bounds, subscriber bounds, and slow-consumer behavior before startup sealing.
  • Per-connection buffers, global subscribers, presence leases, replay results, payloads, and retention are bounded.
  • Where WebSocket is offered, on the hypermedia and React stacks, upgrades require the canonical origin and forge.realtime.v1 subprotocol; control frames and server frames are size-bounded and schema-checked.
  • The product database remains the durable replay authority; pub-sub is a best-effort notification plane and is never a second event store.
  • Broker messages use a versioned fail-closed envelope, ignore their origin node, and distribute presence renewal and explicit revocation without exposing domain state.
  • A slow subscriber is disconnected and never blocks publication.
  • Sessions and authorization are revalidated while a stream remains connected.
  • Presence is ephemeral coarse metadata with TTL and is not audit evidence.
  • Durable product changes commit through domain mutations and integration.events before their minimal realtime notification is published.
  • Conflict responses expose only resource version metadata inside the caller's authorized tenant scope.
  • On the API stacks a product topic may register its own authority, consulted at connect and on every heartbeat, and the stream resolves the bearer session again on every heartbeat.
  • A subscriber whose cursor is ahead of the stored replay head, as after a restore, receives a collaboration.resync signal carrying the head before any event.
  • The API stacks deliver over server-sent events only, with no WebSocket route or dependency, and clients resume with Last-Event-ID.
  • Product code publishes through the one realtime.Publisher the composition root provides under collaboration.realtime.publisher. It coalesces each tenant, topic, resource and event type to one event per topic window, one second by default, refuses an event its topic cannot carry at once, and is closed before the service so held events are sent.
  • The Swift RealtimeClient sends the session only in the Authorization header, checks topic and resource before any request, ends on a refusal it cannot outlast, and otherwise reconnects with bounded, jittered backoff.
Full capability reference
Explore Sync and realtime

Feedback and roadmap

Data & collaborationPrivate product requests and an independently curated public roadmap, with operator review, safe links and deliberate publication.1 capabilityFeedback and roadmapInspect feature

Private product requests and an independently curated public roadmap, with operator review, safe links and deliberate publication.

API routes
15
data tables
4
migrations
1
declared rules
5

Feature source before its dependencies. Stack support is listed for each capability.

Feedback and roadmapcollaboration.feedbackv0.1.2

Private product requests, requester history, operator review and independently curated public plans, installed as owned source.

In the product
People submit private ideas, read their own request history and follow published plans. Operators use separate review and curation views. Internal notes and other customers’ requests stay out of personal history.
Across the API
Owner operations read and submit private requests. Verified operator sessions manage reviews and curated plans. A public read returns only deliberately published plans.
On the server
Stores stable request identities, exact revisions and atomic review history. SQLite and PostgreSQL serialize quota and order changes. Account erasure removes private requests while preserving independent plans.

What it guarantees

  • Request bodies and internal notes are never automatically public
  • Same-intent retries retain the request identity
  • New release claims require product evidence

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Configure verified product operators and a release-evidence resolver. There are no votes, public comments, automatic publication or promised delivery dates. Support conversations remain separate.

Included dependencies (3)
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Accounts and email sign-inidentity.human
Declared rules (5)
  • Private requests, identities and internal notes are never automatically published.
  • Membership does not imply operator access.
  • Every write and its review history commit together.
  • New public release claims require product evidence.
  • Erasing a requester preserves independent public plans.
Full capability reference
Explore Feedback and roadmap

Subscriptions and billing

CommercePlans, checkout, the customer portal and cancellation, feature access derived from each plan, and usage limits.3 capabilitiesPlans and checkoutFeature access by planUsage and limitsInspect feature

Plans, checkout, the customer portal and cancellation, feature access derived from each plan, and usage limits.

API routes
17
data tables
8
migrations
6
declared rules
36

Feature source before its dependencies. Stack support is listed for each capability.

Plans and checkoutcommerce.billingv0.11.6

One subscription per account, whichever provider bills it. Customers start a plan through checkout, manage it in the provider’s portal and cancel it. A hosted provider’s changes arrive as signed events your server verifies.

In the product
Customers see their plan and its price, upgrade through checkout and return to Free after confirming. A plan another provider bills, such as the App Store, is managed in that provider’s settings, and the app says so.
Across the API
GET /billing reads the plan and subscription. POST /billing/checkout starts checkout, POST /billing/portal opens the customer portal and POST /billing/cancel cancels. Providers report changes to POST /integrations/billing/webhook.
On the server
Verifies each provider event’s signature and records it once before the subscription changes. Checkout, the portal and canceling act only on the caller’s own account and admit only the person’s own session unless your product widens them.

What it guarantees

  • One account never holds two live subscriptions
  • Automations and visitors never change a plan
  • Provider secrets and payloads are never stored or shown

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Local billing is for development only. Production needs your own Stripe credentials and price identities, which stay with the host.

Included dependencies (4)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (16)
  • Billing provider events are signature-verified and recorded idempotently before subscription state changes commit.
  • Tenant ownership comes from authenticated sessions or signed provider metadata, never arbitrary request fields.
  • Local billing is development-only; production requires explicit Stripe credentials and price identities.
  • Provider errors, secrets, and webhook payloads are never persisted or rendered.
  • Stripe credentials are bounded header values, provider redirects are refused, and complete responses are byte-bounded.
  • Stripe requests pin one API version, and an event's subscription period is read from the subscription or its plan's item, so a webhook endpoint on another version never loses the period.
  • A Managed Payments session sends none of the parameters Stripe refuses beside it; Managed Payments and automatic tax are never configured together.
  • Checkout charges the quote a page reads: a first-year fee is a verified one-time price on the first invoice only, waived by an introductory offer or a return within its grace, and never offered to a live subscription.
  • An account holds one subscription shared by every provider: an event that grants access may replace another provider's subscription, an event that grants nothing never replaces another provider's subscription that grants, and registered fallbacks run when a change leaves the owner without access.
  • A provider's customer identifier is only sent to that provider, and a projection from a provider without customers never replaces the account's customer.
  • Billing's owner operations act only on the caller's own account, never one named in parameters; billing.get admits the person's session and a key or an assistant with billing:read, checkout, the portal and cancelling admit the person's own session alone unless a product widens them, none is fixed, and an automation flow or a visitor is refused.
  • A local checkout or cancel is a write that commits with its mutation hooks and its idempotency record in the operation's transaction, begun inside Service.Exclusive so one account never gets two live subscriptions; on a hosted provider they are declared external, run inside no database transaction, and record their outcome for a retry.
  • A client offers the portal only when the summary's portal_available is true and cancelling only when cancel_available is true; a subscription another provider bills, such as the App Store, is cancelled in that provider's settings.
  • On every stack, erasing an account cancels at once, with no proration credit, before the erasure transaction and never inside it, every live subscription the server can cancel on the owner's provider customer, and expires their open checkouts; a provider that fails erases nothing. The cancel is a final account deletion step (identity.human's registerFinalAccountDeletionHook), so it runs only after the erasure's rehearsal found no refusal and every step a person can recover from has succeeded. Inside the transaction the guard refuses while a recorded subscription the server can cancel still renews or has a payment due, passes the rehearsal, and a subscription the server cannot cancel, such as the App Store's, never holds erasure.
  • A verified provider event whose owner the store does not have is acknowledged and recorded nowhere, decided by the store reading the owner's row in the event's transaction, never by an error's text; an event the store could not record is answered for a retry.
  • An invoice event's subscription is read from parent.subscription_details at the pinned Stripe version and from the top-level field of older versions.
Full capability reference
Feature access by plancommerce.entitlementsv0.7.7

Features and allowances derived from each plan, decided in one place. A missing, inactive or unknown subscription falls back to the plan you register for it.

In the product
Customers get the features and allowances their plan includes. Your operators can grant one account an exception, with a reason, once the owner’s approval allows it.
Across the API
GET /entitlements lists each feature for the calling account: whether it is on, its limit and why. Operators set or clear one account’s override through POST /entitlements/overrides and .../clear, once the owner’s approval allows it.
On the server
Every capability that decides from plans asks the same entitlement service, so billing never bypasses a decision. Every refusal fails closed and shows nothing about another account.

What it guarantees

  • Billing selects a plan, never bypasses a decision
  • Only listed operators set overrides
  • An override commits with its record or not at all

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Plans and checkoutcommerce.billing
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (8)
  • Billing state selects a plan but never bypasses an explicit entitlement decision.
  • Inactive, missing, or unknown subscriptions resolve through the registered fallback plan.
  • Overrides are tenant-scoped and explainable, and only a verified operator OPERATOR_EMAILS lists sets or clears one, through the entitlements.overrides operations once the owner's approval allows it.
  • Every denial is fail-closed and exposes no billing or override data from another tenant.
  • The composition provides one entitlement service, and every capability that decides from plans uses it rather than building its own, so the product's catalog, fallback plan and supplemental plan reader decide everywhere.
  • An override, the rows its mutation hooks write and the idempotency replay record commit in one transaction or not at all.
  • The account an override is for travels in the request body, never in the path or the query.
  • While the generic starter catalog decides, the server warns at startup until product setup replaces it with ReplacePlans.
Full capability reference
Usage and limitscommerce.meteringv0.6.5

Usage counted against each plan’s allowance, with a ceiling customers may set lower for themselves. Each use is recorded once, and work whose size is known only afterwards can hold its share first.

In the product
Customers see what they have used this period, what their plan includes and the ceiling they chose. When work would pass the ceiling, the app says whose limit stopped it.
Across the API
GET /usage reports each metric for the period. PUT /usage/{metric}/spend-limit sets a ceiling and DELETE removes it, so the plan’s allowance decides again.
On the server
Locks the period’s counter before every limit decision, so simultaneous requests cannot pass the limit together. Each usage write carries an idempotency key, and a plan that cannot be charged more never passes its allowance.

What it guarantees

  • Each use is counted once
  • Concurrent work cannot overrun a limit
  • Usage events are the durable record

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Feature access by plancommerce.entitlements
  • Agent surfaceapi.agent-surface
Declared rules (12)
  • Every usage write has a tenant-scoped idempotency key and immutable quantity.
  • Allowance evaluation, limit comparison, event insertion, and counter increment fail closed.
  • Counters are derived acceleration state; usage events remain the durable evidence.
  • Unknown metrics and disabled entitlements are rejected before storage mutation.
  • Every limit decision for an owner's metric in a period locks that period's counter first, counting recorded usage and unexpired held reservations, so concurrent records and reservations cannot together pass the limit on any database.
  • A reservation closes once, settled under its own key with what the work used or released; a held reservation stops counting at its expiry, and usage settled past it is still recorded.
  • A metric is enforced against the owner's spending limit when they set one, else their plan's allowance.
  • An account whose plan cannot be charged for more never passes its allowance, whatever spending limit it sets.
  • A refusal names whose ceiling stopped the work, and a spending-limit refusal still matches the plain limit refusal.
  • Allowances come from the one composed entitlement service, never a second one built for metering.
  • A metric the account's plan does not include reports included false, so no surface shows it as an allowance used up.
  • The product may replace the whole metric catalog atomically; every metric validates before the previous catalog changes.
Full capability reference
Explore Subscriptions and billing

App Store and license keys

CommerceApp Store subscriptions verified on the server, and license keys with device activations for apps sold directly.2 capabilitiesApp Store purchasesLicense keysInspect feature

App Store subscriptions verified on the server, and license keys with device activations for apps sold directly.

API routes
14
data tables
7
migrations
4
declared rules
18

Feature source before its dependencies. Stack support is listed for each capability.

App Store purchasescommerce.appstorev0.6.6

StoreKit 2 purchases your server verifies before it grants anything. Apple’s server notifications apply once and in any order, and the subscription’s state projects into billing, so every client sees the same plan.

In the product
Customers buy Pro in the iPhone app’s storefront and restore earlier purchases. The app posts each signed transaction to your server, which grants the plan. Linklet’s browser account page then shows Pro as bought in the App Store, which is one way to show it.
Across the API
GET /commerce/appstore/account-token gives the app the token it attaches to a purchase, and POST /commerce/appstore/transactions posts a signed transaction. GET /commerce/appstore/entitlement reads the App Store state and POST /commerce/appstore/reconcile asks Apple for it again. Apple reports changes to POST /integrations/appstore/notifications.
On the server
Verifies every signed payload against Apple’s root, the product’s bundle ID and its environment before any state changes, and applies inputs in signing order. A grace period keeps access, billing retry does not, and a refund revokes it.

What it guarantees

  • Another account cannot claim a subscription
  • A repeated notification changes nothing
  • Production refuses test signatures

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite

App Store purchases happen only inside an Apple app, so a web or React product has no transaction to verify. An Apple product that also serves a browser keeps its purchases on the Apple stack.

Known limit. App Store purchases happen only inside an Apple app, so the web-only starting points do not include them. Reconciliation needs your App Store Server API key, which stays with the host.

Included dependencies (6)
  • Agent surfaceapi.agent-surface
  • Plans and checkoutcommerce.billing
  • Feature access by plancommerce.entitlements
  • Accounts and email sign-inidentity.human
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (11)
  • Every signed payload is verified against Apple's root (or the configured test roots) before any state changes; the bundle id and environment must be the product's.
  • Inputs apply in signing order: a notification delivered late never overwrites a newer state, and a repeated notification id is a no-op.
  • Grace period keeps access, billing retry does not, a trial is not a paid offer, a refund revokes retroactively; the conformance suite is the contract.
  • A subscription belongs to the account that posted its transaction or whose account token it carries; another account cannot claim it.
  • Billing state is a projection; commerce.entitlements makes the access decision.
  • The projection never replaces another provider's subscription that grants, nor the account's customer; the billing fallback restores the App Store subscription that grants longest once the recorded one stops granting.
  • The Apple gate honors the server's decision whichever provider granted it, never from decisions older than maxAge and, while the App Store grants, never past its until.
  • Production refuses test roots, Xcode mode and the sandbox environment.
  • The test signer products use, appstoretest, runs only in test binaries and is never imported by production code; Apple's roots refuse what it signs.
  • Every operation declares its Actors: the account token and the transaction the app posts admit the owner's own session alone, and a product that widens them accepts that exposure; the App Store state and reconciliation admit the owner, a scoped key or an assistant, never a flow. Authorize checks no kind. Apple's notification endpoint is the one public plain route.
  • A refused reconciliation or notification carries Retry-After, the wait its refusing window has left (ADR 0077).
Full capability reference
License keyscommerce.license-keysv0.4.5

License keys for apps you sell directly, issued from billing plans. Each key activates up to the plan’s number of devices, and each device holds a signed lease it verifies offline.

In the product
Customers enter a key or start a trial, and the app keeps working offline on its lease. On the web starting points, customers manage their keys and devices on a licenses page and remove a device to free its seat. In Linklet the License section is in the Mac app, which these screens do not show.
Across the API
POST /licenses/activate, /licenses/heartbeat and /licenses/deactivate take the key as their credential, POST /licenses/trial starts a trial and GET /licenses/public-key publishes the verifying key. GET /licenses lists the account’s keys and DELETE /licenses/{licenseId}/devices/{deviceId} frees a seat.
On the server
Issues a key inside the billing transaction, once per subscription. A plan change follows the plan, and a lapsed subscription expires the key at its period end. Leases are Ed25519, bound to one device and never run past the license’s expiry.

What it guarantees

  • Re-activating a device never takes a second seat
  • A device gets one trial per product
  • Erasing an account revokes its keys

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. A lease stays valid offline until it ends, 7 days by default, so a revoked key stops a device that never checks in only then. Keys are not tamper-proof copy protection, and LICENSE_SIGNING_PRIVATE_KEY stays with the host.

Included dependencies (6)
  • Agent surfaceapi.agent-surface
  • Plans and checkoutcommerce.billing
  • Accounts and email sign-inidentity.human
  • Provider readinessprovider.readiness
  • Durable rate limitssecurity.rate-limit
  • Secret referencessecurity.secret-references
Declared rules (7)
  • A lease is the only thing the app trusts: signed by the product's Ed25519 key, bound to one device, bounded by the lease and license expiries, verified offline with the embedded public key.
  • Keys are issued from billing inside the billing transaction, once per subscription; a plan change follows the plan, a lapsed subscription expires the key at its period end, and the product revokes a refunded key through the internal API.
  • Device activations count against the plan's limit; a deactivated device frees its slot; the same device re-activating is idempotent.
  • A device gets one trial per product and so does an account that starts one signed in; a trial is started without a key and never extended, and an account's other devices share its window.
  • The key-authenticated routes are public plain routes with one contract fragment on every server profile; activation is limited per peer and in all through security.rate-limit, the others by an in-process throttle with the same limits. The account's keys are listed and its slots freed only through operations that declare the owner's own session as their only actor; they are not fixed, so a product may widen them and owns that exposure.
  • Account erasure revokes, detaches and anonymizes the account's keys and detaches its trials; an erased person's key no longer activates or renews.
  • On the web profiles the licenses page and its removal form are the signed-in owner's alone, refuse a cross-site form, and free a slot through the same store call as licenses.devices.remove; a trial's bearer is the session's own token, and a cookie never names the trial's account.
Full capability reference
Explore App Store and license keys

Email and notifications

CommunicationTransactional email, in-app notifications, push to Apple devices, digests, and the preferences and routing rules behind them.7 capabilitiesTransactional emailIn-app noticesNotification choicesMessage templatesActivity digestsPush notificationsNotification routingInspect feature

Transactional email, in-app notifications, push to Apple devices, digests, and the preferences and routing rules behind them.

API routes
16
data tables
10
migrations
11
declared rules
56

Feature source before its dependencies. Stack support is listed for each capability.

Transactional emailcommunication.emailv0.4.2

Transactional email through one mailer, with a development inbox and production providers. Each send carries its idempotency key to the provider, so a retried send is recognized.

In the product
Users receive sign-in links, invitations and notices by email. In development every message lands in a local inbox instead of being sent.
Across the API
GET /dev/mail lists captured messages outside production, for the configured operator only. Other capabilities send through the mailer rather than a route of their own.
On the server
Constructs one provider at startup behind the mailer interface. Production names its provider and never permits the development inbox, and providers that carry credentials refuse unsafe endpoints and redirects.

What it guarantees

  • Production never uses the development inbox
  • A retried send is recognized by its key
  • Provider secrets stay with the host

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Production needs your own Resend key or SMTP relay, which stays with the host.

Included dependencies (2)
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (7)
  • Production has an explicit provider and never permits devbox.
  • Secret material remains host-owned and never enters composition artifacts.
  • The development inbox is absent from production and restricted to a configured operator.
  • Provider adapters are constructed once and injected behind the Mailer interface.
  • Credential-bearing HTTP providers reject unsafe endpoints, refuse redirects, and bound complete responses.
  • A send carries its idempotency key to the provider: Resend deduplicates on it, SMTP derives a stable Message-ID from it, and the devbox returns the earlier capture.
  • EraseOutboundMailTx deletes every captured development message to an address, bare or under a display name and in any case, inside the caller's erasing transaction. Mail registers no account deletion hook, because it can be composed without identity.human; identity.human's DeleteUser (0.4.2 or later) calls the eraser with the account's address, so a deleted account's captured mail goes in the deletion's transaction.
Full capability reference
In-app noticescommunication.messagingv0.4.2

Durable in-app notices and queued email, each checked against the person’s choices before anything is stored.

In the product
Users read notices in the app’s activity list and mark them read. A notice links only to a page inside your product.
Across the API
GET /notifications lists the caller’s notices and POST /notifications/{messageId}/read marks one read.
On the server
The message, its delivery records and its email job commit together, so nothing is queued for a message that did not save. Ending someone’s access withdraws their messages and any unsent email in the same transaction.

What it guarantees

  • An idempotency key binds to one message
  • Read state never crosses owners
  • Email is delivered at least once

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Transactional emailcommunication.email
  • Notification choicescommunication.preferences
  • Background jobsworkflow.jobs
Declared rules (9)
  • Every send evaluates the registered topic for every channel it may use, the request's Channels or both, before durable state is created; SendPlanned records a caller's decision, such as communication.routing's, without asking again.
  • A caller-supplied idempotency key binds to exactly one tenant, user, and immutable message body.
  • Message creation, delivery records, and email job enqueue commit atomically.
  • Action URLs are relative product paths, email delivery is at least once, and tenant read state never crosses owner boundaries.
  • A product withdraws a scope's messages, and so any unsent email, in the transaction that ends a recipient's access.
  • A repeated send enqueues a still-pending email again, so a dead email job runs again, and the email job passes its idempotency key to the mailer.
  • Inside a write transaction a product sends with SendTx, which writes in the caller's transaction; Send begins its own and is called only where no write transaction is open.
  • Messaging delivers in_app and email, named as communication.preferences names them, and refuses push, which communication.routing sends.
  • communication.routing reaches the in-app inbox by name, communication.messaging.inbox, and each routed delivery records one message under its delivery key.
Full capability reference
Notification choicescommunication.preferencesv0.3.3

Topics your product registers, each with in-app, email and push channels users turn on and off. A channel the topic requires stays on.

In the product
Users choose, topic by topic, whether Linklet tells them in the app, by email or by push. Security notices always arrive by email.
Across the API
GET /communication/preferences lists the caller’s topics and channels. PUT /communication/preferences/{topic}/{channel} sets one and DELETE returns it to the topic’s default.
On the server
Evaluates the registered topic for every channel before a message exists. Unknown topics and channels fail closed, and a topic the product hides still honors a channel the person turned off.

What it guarantees

  • Required channels cannot be turned off
  • Only the owner reads or changes their choices
  • Unknown topics never default to on

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (7)
  • Every topic and supported channel is registered by trusted product source before evaluation.
  • Required topic channels cannot be disabled by stored or requested preferences.
  • Decisions are scoped by tenant and user, with server-derived identities at the storage boundary.
  • Unknown topics and channels fail closed rather than inheriting a permissive default.
  • The preference operations admit only the owner's session and API keys and read and change only that owner's own choices; an assistant acting for them is refused by kind and does neither.
  • One channel vocabulary, in_app, email and push, is shared with communication.routing and communication.messaging.
  • A topic the product hides is never listed, set or reset, and still evaluates to each person's stored choice, so hiding never turns on a channel a person turned off.
Full capability reference
Message templatescommunication.templatesv0.2.2

Product-owned templates for every subject, text and HTML body, with declared inputs and previews rendered from examples.

In the product
Every email and notice reads from one set of templates, so copy stays consistent across capabilities.
Across the API
GET /communication/templates lists the registered templates and GET /communication/templates/{templateId}/preview renders one from its declared examples.
On the server
Parses and sample-renders every template at startup, so broken copy stops the server rather than a send. HTML is escaped by context, and subjects refuse line breaks.

What it guarantees

  • Missing or unknown variables are refused
  • Previews never need customer data
  • Every input and output has a size bound

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (1)
  • Product settingsproduct.settings
Declared rules (5)
  • Templates declare every input and reject missing or unknown variables.
  • HTML uses contextual escaping while subjects and text use bounded text rendering.
  • Registration parses and sample-renders templates so invalid copy fails at composition startup.
  • Rendered subjects reject line breaks and all inputs and outputs have explicit size bounds.
  • Preview uses declared examples and never requires production customer data.
Full capability reference
Activity digestscommunication.digestsv0.5.2

Frequent activity collected into daily or weekly summaries, closed on a schedule and mailed once.

In the product
Users choose how often digests arrive, or turn them off. Turning digests off or back on discards what was waiting, so they never get mail about activity from while digests were off.
Across the API
GET /communication/digests reads the caller’s cadence and windows, and PUT /communication/digests sets the cadence.
On the server
Rechecks the cadence and the email choice when a window closes, and commits the closed window with its delivery job. Each digest holds at most 500 items.

What it guarantees

  • Each item is collected once
  • A window and its delivery commit together
  • Only the signed-in person reads their digests

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (6)
  • Transactional emailcommunication.email
  • Notification choicescommunication.preferences
  • Message templatescommunication.templates
  • Accounts and email sign-inidentity.human
  • Product settingsproduct.settings
  • Background jobsworkflow.jobs
Declared rules (13)
  • Collection is idempotent and tenant and user scoped.
  • Window close orders items by group, occurrence, and ID and caps each delivery at 500 items.
  • Cadence and channel preference are rechecked when a durable window closes.
  • Window state and its idempotent delivery job commit atomically.
  • Suppressed windows consume their items and retain evidence without enqueueing email.
  • Only the composed sweep worker, product code calling SweepCommunicationDigestsNow, and the product-registered delivery job can close or deliver digests.
  • A person's cadence before they choose one is daily only when their choice for the digest topic's email channel, or the topic's default, lets digests be mailed; adding an item never replaces a chosen cadence.
  • A person whose cadence is off collects no items, and setting a cadence to off or back on from off discards the items waiting, so opting in never mails activity from while digests were off.
  • Template rendering, recipient resolution, and mail delivery use host-owned services and secrets.
  • A product withdraws a scope's undelivered items, including a queued digest's, in the transaction that ends a recipient's access; an emptied queued digest is suppressed.
  • The API routes read and set only the signed-in person's own cadence and windows.
  • Inside a write transaction a producer adds items with AddCommunicationDigestItemTx, which writes in the caller's transaction; AddCommunicationDigestItem begins its own and is called only where no write transaction is open.
  • A digest email links to digest settings only on a page the product serves, through a host-configurable path on its own origin; the API stacks send no settings link by default.
Full capability reference
Push notificationscommunication.pushv0.5.2

Push notifications to the owner’s Apple devices through APNs. Every APNs answer is classified as delivered, retry, drop the device or failed.

In the product
The app registers its device token once per installation. Users see their devices, remove one and send themselves a test.
Across the API
GET /push/devices lists devices and POST /push/devices registers one. DELETE /push/devices/{deviceId} removes a device and POST /push/devices/{deviceId}/test sends it a test.
On the server
Signs APNs provider tokens with a key that never leaves the server’s configuration. A device APNs no longer delivers to is forgotten, and a refusal that needs a change is never retried.

What it guarantees

  • A device token is never returned
  • A token belongs to one owner at a time
  • An owner keeps at most 50 devices

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite

Push reaches an Apple app, which registers through the API server profile. A web or React product has no device to register until ADR 0064's additive clients let an Apple client sign in to it.

Known limit. Push needs an Apple app and your APNs key, which stays with the host. Without the key, pushes are recorded for development and not sent.

Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (9)
  • A device is registered, removed and tested only by the person's own signed-in session, unless the product widens those operations to API keys, and listed by that session or a key holding devices:read; an assistant acting for them does none of it.
  • A token belongs to one owner at a time and an installation keeps the token it registered last; an owner keeps at most 50 devices.
  • A device a session registers is bound to that session: signing out, a revocation and the sweep of an expired session remove it in the session's own transaction, and ending every session of a person removes all of their devices.
  • A device's token is never returned by an operation.
  • The provider token is an ES256 JWT reused for 50 minutes and never renewed within 20, and the APNs key never leaves the process's configuration.
  • Every APNs answer is classified before anything is retried: a device APNs no longer delivers to is forgotten, unless the app registered it again after APNs saw it inactive, and a refusal that needs a change is never retried.
  • Production requires APNS_TEAM_ID, APNS_KEY_ID, APNS_PRIVATE_KEY and APNS_TOPIC; elsewhere, without the key, pushes are recorded and not sent, the recorder keeps the newest 1,000, and APNS_TOPIC set alone is the product's app, the topic a registration that names none takes.
  • Recorded pushes are shown as JSON at /dev/push only outside production and only to a direct loopback request. A product whose pushes reach APNs answers 404 there, and a device's token is never shown.
  • The Apple client's registration keeps its keychain items per server origin and account; the app-wide items of PushRegistration(client:) are deprecated, and a storage closure it is given is @Sendable.
Full capability reference
Notification routingcommunication.routingv0.4.2

One routing table instead of scattered sends. Routes match events and name the channels, people and attempts, and a dry run shows what an event would send.

In the product
Your product decides which events reach users by push, email or the in-app inbox. Owners can try an event against their routes before relying on them.
Across the API
GET /routing/routes reads the table and PUT /routing/routes replaces it. POST /routing/try runs a dry run and GET /routing/deliveries lists what was sent.
On the server
Records deliveries and enqueues their jobs in one transaction. Each delivery is tried at most its route’s attempts, up to 10, and ends sent, failed, unreachable or suppressed with a reason.

What it guarantees

  • A person hears once per channel per event
  • Routing an event again sends nothing new
  • Only the owner replaces their routes

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (4)
  • Transactional emailcommunication.email
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • Background jobsworkflow.jobs
Declared rules (6)
  • Every matching route fires unless another matching route suppresses it, whatever the table's order.
  • A person hears once per channel for one event however many routes name them, and routing an event again sends nothing new.
  • Deliveries are recorded and their jobs enqueued in one transaction, which can be the caller's, so nothing is sent for what did not commit.
  • A person's devices and address are looked up only when a delivery is sent.
  • A delivery is tried at most its route's attempts, at most 10, and ends sent, failed, unreachable or suppressed, which is held back before an attempt by the person's choice or a delivery guard and names that reason; a retry APNs asks for waits the delay it names.
  • Only the owner reads their routes and deliveries, signed in or with an API key holding routes:read, and only their own signed-in session replaces the routes; an assistant or a flow acting for them does neither.
Full capability reference
Explore Email and notifications

Customer support

CommunicationCustomer conversations from the web, an app or an agent, optional model triage and drafts for operator review, and time-limited support access.3 capabilitiesSupport casesSupport assistantSupport accessInspect feature

Customer conversations from the web, an app or an agent, optional model triage and drafts for operator review, and time-limited support access.

API routes
46
data tables
12
migrations
9
declared rules
33

Feature source before its dependencies. Stack support is listed for each capability.

Support casescommunication.casesv0.5.6

A customer’s conversation with your operators, opened from the web, an app or an assistant with its owner’s key, and worked on a board of four fixed lanes your product may relabel.

In the product
Customers open a case with a subject, a topic and what happened, read the replies and answer in the same thread. When it is solved they can mark it resolved and rate it once, from 1 to 5.
Across the API
GET /cases lists the caller’s cases and POST /cases opens one. POST /cases/{caseId}/messages replies, POST /cases/{caseId}/resolve resolves it and POST /cases/{caseId}/rating rates it. Operators work the desk under /desk/cases.
On the server
Commits each change with its messages, timeline, audit receipts, events and notifications in one transaction. An assistant writes triage and reply drafts, and nothing reaches the customer unless an operator sends it. A case waiting on its customer is nudged after 5 days and resolved after 10 by default.

What it guarantees

  • An internal note never reaches the customer
  • Customers read and write only their own cases
  • Receipts never carry a message’s words

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. The desk drafts from built-in rules that need no model. A model assistant comes with the separate cases assistant capability, runs on your own provider key within a monthly desk budget, which stays with the host, and falls back to the rules when it fails.

Included dependencies (6)
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Notification routingcommunication.routing
  • Filescontent.assets
  • Accounts and email sign-inidentity.human
  • Domain eventsintegration.events
Declared rules (10)
  • The lanes are new, in_progress, waiting_on_customer and resolved, and never more; products relabel them.
  • A customer, or their key or assistant with the cases scopes, reads and writes only their own cases, and only the person rates one; operators are verified accounts OPERATOR_EMAILS lists, working the desk on their own session unless the product widens a desk operation, a kind of caller an operation does not admit is refused for its kind, and to any other account the desk does not exist.
  • An operator's internal note is never shown to the customer.
  • CASES_DESK=off serves no desk from that deployment: every desk operation, and an operator's read of a case's file, answers a session as if it did not exist, while a key or an assistant is refused for its kind first as on any deployment, customers keep opening and following their cases and operators are still told about them.
  • The assistant's triage and drafts are suggestions: nothing reaches a customer unless an operator sends it.
  • Every change to a case, its messages and timeline, its receipts on the owner's audit chain, its integration events and its notifications commit together or not at all.
  • Receipts and integration events carry identifiers, lanes and kinds, never a message's words or anyone's address.
  • Deleting an account deletes its cases, threads and timelines.
  • A notification's case and desk links are http or https URLs or a scheme the product's app registers, never a script, data, file, mail or telephone link.
  • The default categories offer billing only when commerce.billing is composed; a product's own vocabulary decides for itself.
Full capability reference
Support assistantcommunication.cases.assistantv0.3.2

A bounded model assistant for your own support desk, using your provider key and a monthly desk budget.

In the product
Operators inspect private suggested triage and reply drafts before deciding what to send. Customers receive no automatic model reply.
Across the API
The desk requests private suggestions through its existing support workflow; sending remains a separately authorized operator action.
On the server
Reserves and settles actual model usage in the durable ledger, keeps suggestions private, and falls back to rules when budget is exhausted or a provider fails.

What it guarantees

  • An operator reviews before sending
  • Actual model usage settles durably
  • An exhausted budget adds no model spend

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Bring your model-provider credentials and desk budget. Local reference checks do not qualify a live provider.

Included dependencies (2)
  • Support casescommunication.cases
  • AI tools and budgetsai.tools
Declared rules (5)
  • The product's provider key is a server secret and never reaches a client.
  • Every model call reserves tokens against the desk's period budget before it runs, under its own key and for a bounded time, and settles what it used; a hold a lost call left behind is released by the next hold.
  • An exhausted budget, a provider failure and malformed model output all fall back to the rules assistant.
  • A suggestion records which assistant produced it, so an operator can see whether a model or the rules answered.
  • Nothing the model writes reaches a customer; an operator uses, edits or discards every suggestion.
Full capability reference
Support accessoperations.supportv0.4.4

Delegated access for your support team. An operator asks to read an account for a set time, its owner approves, and every read, note and action is recorded. Support never signs in as the customer.

In the product
Customers see who from support is asking, why and for how long, then approve, deny or end the access. Linklet’s Support access, above, is one way to show it.
Across the API
Operators ask with POST /support/grants and open approved access with POST /support/grants/{grantId}/activate. Owners read requests at GET /support/access and answer with POST /support/access/{grantId}/approve, deny or revoke.
On the server
Binds each approved grant to one approval, given from a sign-in in the last 15 minutes, and fails closed on every read, note and action once access expires or ends. Resources return only their registered fields, with confidential values masked and restricted ones left out.

What it guarantees

  • Only the person approves, never a key or assistant
  • Operators never act as the customer
  • Ended access fails closed on every read

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. The operator console at /ops/support, with break-glass access, comes with the hypermedia and React stacks. On the API and Apple stacks operators work through the support API, which offers no break-glass access.

Included dependencies (8)
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Data classificationdata.classification
  • Retention and legal holdsdata.retention
  • Accounts and email sign-inidentity.human
  • Operator checksproduct.operations
  • Permissions and custom rolessecurity.authorization
  • Approvalsworkflow.approvals
Declared rules (18)
  • Cross-tenant directory and resource reads require an explicit AUTH-01 support role and immutable audit evidence.
  • Delegated grants preserve the operator identity and never mint a customer session.
  • The grant's tenant is an opaque product reference; the product's tenant authority (by default, the approver is the tenant) decides who may approve, and is checked when access is requested, approved and activated.
  • Approved grants bind to one customer approval; break-glass grants require recent authentication, exact tenant confirmation, and expire within fifteen minutes.
  • The approver approves only from a recently authenticated session, may deny or end access at any time, and owns the approvals, case-note retention and action receipts that need a person.
  • Expired and revoked grants fail closed on every read, note, and action instead of relying on UI state.
  • Resource providers expose only registered bounded fields; confidential values are masked and restricted values are omitted.
  • Case notes are internal, bounded, append-only, and never copied into customer-visible audit metadata.
  • Every case note atomically creates a retention assignment owned by the grant's approver under the plugin-owned confidential classification and review policy.
  • Support actions map to registered product operations, preserve delegation identity, use idempotency receipts, and require exact confirmation.
  • High-risk action approval binds the tenant, grant, action, idempotency key and any subject, and cannot authorize a different execution.
  • Support source never accepts SQL, shell commands, raw schemas, credentials, or unregistered data fields from request input.
  • Each resource kind registers how long after activation a grant may read it, from 5 minutes to 24 hours, 8 hours by default; a grant may ask for no longer than the longest registered kind.
  • SUPPORT_OPERATOR_EMAILS only starts the support desk: at startup it makes each listed verified account a support admin when it holds no support role yet, and never overrides a role an admin set. communication.cases' OPERATOR_EMAILS is a separate setting.
  • Development starts the desk with one admin and production with only the accounts it lists: the API stacks and Apple's server name the example account in .env.development, which production refuses to load, and on the hypermedia and React stacks an empty SUPPORT_OPERATOR_EMAILS seeds the kernel's development operator only while development routes are on, which production refuses.
  • Only a support admin lists and changes support roles, never their own, and every change, including a seeded one, is recorded in the audit trail.
  • Operator operations declare owner.session as their only caller and then require a support role; unless the product widens them with ConfigureOperationActors, a key or an assistant never acts as a support operator and is refused actor_not_allowed.
  • support.access.approve declares owner.session alone and is fixed, so a product may narrow it and never widen it, and needs a session signed in within the last 15 minutes; keys and assistants may list, deny and end access to their account but never approve.
Full capability reference
Explore Customer support

Files and publishing

Content & filesPrivate file storage, uploads and revision history, public pages on custom domains, moderation, and rendered documents to download.7 capabilitiesFile storageFilesAsset revisionsPublic pagesCustom domainsContent reviewRendered documentsInspect feature

Private file storage, uploads and revision history, public pages on custom domains, moderation, and rendered documents to download.

API routes
48
data tables
16
migrations
19
declared rules
70

Feature source before its dependencies. Stack support is listed for each capability.

File storagestorage.objectv0.1.4

One private store for the bytes other capabilities keep: a local folder in development and an S3-compatible bucket in production, behind the same interface.

In the product
Nothing to show on its own. Users’ files and rendered documents live here, and they reach them only through the capability that owns them.
Across the API
No routes of its own. Files and rendered documents read their bytes through their own routes, which check access first.
On the server
Writes are bounded, measured with SHA-256 and create-only, so a retry never replaces an object. Keys are validated and relative and never come from a client, and each read is bounded by the capability asking.

What it guarantees

  • Private objects never get provider URLs
  • An existing object is never overwritten
  • Local storage is refused in production

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Production needs an HTTPS S3-compatible bucket and its access keys, which stay with the host. Readiness checks show reference names only.

Included dependencies (2)
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (5)
  • Object keys are validated, relative, traversal-safe, and never accepted directly from untrusted clients.
  • Writes are bounded, SHA-256 measured, and create-only so retries cannot silently replace an existing object.
  • Reads are bounded by the consumer and private objects are never exposed through provider URLs.
  • Local storage is development-only; production requires explicit HTTPS S3-compatible configuration.
  • Credential material remains host-owned and readiness output contains reference names only.
Full capability reference
Filescontent.assetsv0.8.4

Bounded uploads for an account or a team. Each file records its size, type and SHA-256, and its bytes stay in private storage.

In the product
Users add images, PDFs and plain text, then download or delete them. Linklet saves a link’s QR poster here, and saving it again returns the same file.
Across the API
POST /assets?filename= uploads raw bytes, and an optional Idempotency-Key makes a retry return the same file. GET /assets lists files, GET /assets/{assetId}/content downloads one and DELETE /assets/{assetId} removes it.
On the server
Detects each file’s type from its bytes and keeps only allowed types; SVG and HTML are refused. Content is served with nosniff and no-store, and bytes whose upload never committed are swept after 15 minutes.

What it guarantees

  • A retry under the same key stores one file
  • Client media types are never trusted
  • Erasing an account removes its bytes

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Files are private downloads. There is no public delivery network or malware scanning.

Included dependencies (3)
  • File storagestorage.object
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (14)
  • Tenant identity comes from the authenticated owner actor and is the first argument to every scoped store method.
  • Uploaded bytes are bounded, detected server-side, allowlisted, and never trusted from client media metadata.
  • Metadata records the immutable storage key, byte count, media type, and lowercase SHA-256 evidence.
  • Private content is opened only after tenant-scoped metadata authorization and is served with nosniff and no-store policy.
  • Database and object-provider mutations use compensation plus recoverable deleting state rather than claiming cross-system atomicity.
  • Active content types such as SVG and HTML are denied by default.
  • Object I/O never runs inside a membership transaction: an upload reserves its key under the lock, writes outside it, and commits after checking membership and the reservation again.
  • A commit that reports an error is settled by reading the record; bytes are removed only after the reservation is given up and no file holds them.
  • Every upload, account or team, reserves its object key before writing, and a bounded sweep removes bytes whose record never committed after the grace.
  • Erasing an account queues the bytes of its files in the erasing transaction, without object I/O, and the capability's own sweep removes them; removing bytes already gone succeeds.
  • Erasure and every account file record hold the owner's account row first, so no file recorded during an erasure outlives it with nothing naming its bytes.
  • Deleting a team marks its ready files deleting in the deleting transaction through the teams OnTeamDeleted seam, found by interface, and the sweep removes their bytes.
  • An account upload under a key stores at most one file: sending it again returns that file, other bytes or another name under the key are a conflict, and the key stays with the record after deletion.
  • Erasing an account asks the capability sweep to run at once after the erasure commits, through the all-sessions-revoked hook.
Full capability reference
Asset revisionscontent.revisionsv0.1.2

An owner-scoped revision tree for assets: immutable versions, branches, comparison, labels, pins and explicit restore.

In the product
Users follow file history, compare versions, label a change and pin or restore a known-good revision.
Across the API
The revision API saves versions and follows their lineage; explicit compare, pin and restore operations keep the same account boundary.
On the server
Keeps immutable revision facts and private object references, and includes revision metadata in export and account erasure.

What it guarantees

  • Versions preserve their lineage
  • Restore is an explicit change
  • Export and erasure include revision metadata

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Configure the object-storage provider and your product’s asset rules before accepting real files.

Included dependencies (6)
  • File storagestorage.object
  • Filescontent.assets
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Accounts and email sign-inidentity.human
  • Domain eventsintegration.events
Declared rules (13)
  • A revision never changes an asset object: it points at a ready content.assets file of the same owner or team, and its file, hash, size, media type, parent and creator never change once written; database triggers refuse it. A move gives it the next number in its new lineage and records the old one.
  • A lineage numbers every revision it gains from one counter, across branches, restores, moves and merges, so a number is never used twice in a lineage.
  • A revision whose content hash equals its parent's is refused with revision.unchanged, and so is a restore of the content current already holds.
  • A restore is a new revision parented on the chosen one, titled as its restore unless the caller names it, and current moves to it; nothing earlier changes.
  • current is reserved: it moves only when a revision is created or restored, and no caller pins or unpins it; products declare other pins in REVISIONS_PINS or code, at most 15, and a pinned revision is never pruned.
  • Pruning keeps a tombstone with its id, number, parent, label and facts, so the tree stays whole; only the file goes, and only when no other live revision holds it.
  • Facts are appended by namespace, never edited or deleted; only the product writes the system namespace, and each namespace holds at most 16 KiB per revision.
  • Lineages are scoped to an account or a team, and every store method takes the caller and checks the scope inside the write transaction; another owner's lineage or file answers not found.
  • Moves, merges and splits stay within one owner and scope: a moved revision keeps its id, content and facts, takes the next number in its new lineage and records where it came from.
  • A file a live revision holds refuses deletion through content.assets, for accounts through the asset mutation hook and for teams through the team asset access hook.
  • Every change appends an audit receipt to its owner's trail and an integration event carrying identifiers, numbers, pin names and kinds, never a title, note, filename or fact.
  • A write with a request key runs once: sending it again returns the first result, and the same key with other arguments is a conflict.
  • Pruning and deleting admit the person and their keys only; an assistant restores, pins and labels, and a product widens that only with ConfigureOperationActors.
Full capability reference
Public pagescontent.public-pagesv0.5.3

Pages users write as drafts and publish at a public address. Guards your product registers decide what may go public.

In the product
Users write a page, publish it at its own address and take it down again. In Linklet, each account has one About page, edited in the browser.
Across the API
POST /pages drafts a page and PUT /pages/{pageId} rewrites it. POST /pages/{pageId}/publish and .../unpublish change what is public, and GET /s/{slug} serves the published page.
On the server
Runs your product’s guards before publishing. A refusal carries a code and a sentence for the owner, and a guard that cannot decide refuses. Only the owner’s own session publishes, and the publish limit is counted in the same transaction.

What it guarantees

  • Public output is escaped
  • Unpublishing keeps the page’s history
  • Each slug names one published page

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Public responses are cached for 60 seconds, so a shared cache may serve a page for up to a minute after it is unpublished.

Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (7)
  • Drafts are tenant scoped and become publicly readable only through an explicit conditional publish transition.
  • Each page operation names its callers: listing, drafting, rewriting and unpublishing admit the owner's session, keys and assistants; publishing, which chooses what becomes public, admits only the owner's own session unless the product widens it. No operation admits a flow, and a refused kind answers 403 actor_not_allowed.
  • Globally unique validated slugs map to one published page and never derive tenant authority from the request.
  • Product-owned guards run before publishing, and those that ask for drafts before saving one; a refusal carries a namespaced code and a sentence for the owner, and a guard that cannot decide fails closed without changing page state; a publish or rewrite lands only on the page content its guards checked.
  • Public output is escaped, cache-bounded, conditionally revalidated, and carries canonical SEO metadata.
  • Unpublishing removes the page from public lookup without deleting its durable authoring history.
  • An owner's published pages never exceed the deployment's or the product's publish limit, counted in the transaction that publishes.
Full capability reference
Custom domainsnetwork.domainsv0.5.2

Hostnames users register for their public content, proved by a DNS record they add, with routing and certificate readiness reported by your host or edge.

In the product
Users register a hostname, add its record and verify it. In Linklet, domains are managed through the API, and a verified domain answers only its owner’s short links.
Across the API
POST /domains registers a hostname and returns its challenge once. POST /domains/{domainId}/verify checks it, GET /domains lists registrations with their state and DELETE /domains/{domainId} removes one.
On the server
Stores each challenge only as a SHA-256 digest. Only a verified registration owns a hostname: a pending one reserves nothing and lapses after seven days, and the first to verify wins while later claims are refused without naming the owner.

What it guarantees

  • Corbelo never changes DNS
  • Host reports cannot establish ownership
  • A custom domain never signs anyone in

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Production proves ownership with a DNS TXT record the customer adds, and routing and certificates come from your host or edge. Linklet verifies reserved .test names locally, with no DNS record. There is no re-verification yet.

Included dependencies (1)
  • Accounts and email sign-inidentity.human
Declared rules (9)
  • The product owns desired hostname and ownership state; the host or edge provider owns routing and certificate observed state.
  • Ownership challenges are random, revealed once, and stored only as SHA-256 digests.
  • Only a verified registration owns a hostname. A pending registration reserves nothing, lapses after seven days, and each carries its own challenge; the first verification wins and later ones are refused without naming the owner.
  • Production ownership verification uses bounded DNS TXT lookup and never changes DNS.
  • Local verification accepts only reserved `.localhost` and `.test` names and is rejected in production.
  • Host reports cannot establish ownership and apply only to an already verified exact hostname.
  • Only the account that registered a domain removes it; removing a verified domain releases the hostname at once.
  • ResolveHost answers only a verified registration's owner and selects whose public content to serve; it never authenticates a request or authorizes a mutation.
  • VerifiedDomainHolder answers another capability's store code who holds the verified registration of an exact hostname, inside the caller's transaction; a caller asks again at every use, so removing a registration withdraws the proof everywhere at once.
Full capability reference
Content reviewcontent.moderationv0.2.4

Quarantine, review and appeal for content your product registers, with a publication guard that needs an approved case for the exact content.

In the product
Users see every review of their content and appeal a rejection once, and operators decide from a review desk. In Linklet, an About page with active content such as a script, or one asking visitors for a password or money, waits for a reviewer.
Across the API
POST /moderation/cases sends a resource for review, GET /moderation/cases lists the owner’s cases and POST /moderation/cases/{caseId}/appeal appeals. The desk under /moderation/desk answers only an operator’s own session.
On the server
Keeps content digests and decision evidence, never the content itself. Before publishing, the guard reads the current content again and needs an approved case with its exact digest, so an edited page needs a new approval.

What it guarantees

  • Raw content never enters moderation storage
  • One appeal per rejected case
  • Each decision confirms the exact case

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Owners send their own content for review. Visitors cannot report a page yet, and an approved case stays approved.

Included dependencies (7)
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Data classificationdata.classification
  • Retention and legal holdsdata.retention
  • Accounts and email sign-inidentity.human
  • Permissions and custom rolessecurity.authorization
  • Explainable risk decisionssecurity.risk
Declared rules (9)
  • Products register a subject-specific resolver, bounded signal extractor, deterministic risk policy, and optional pre-submission enforcement guard.
  • Moderation storage contains content digests and decision evidence, never raw subject content or challenge tokens.
  • Owner, subject, and resource identity come from trusted server and product boundaries, never reviewer-supplied ownership fields.
  • A publication guard re-resolves current content and requires an approved case with the exact current content digest.
  • Quarantine, reject, appeal, and appeal-resolution state transitions are conditional and guarded in SQLite.
  • Reviewer content is resolved only after an explicit moderator role and AUTH-01 permission check.
  • Review and appeal resolution require exact case confirmation and bounded reasons.
  • Case creation atomically appends audit evidence and assigns the plugin-owned confidential retention policy.
  • Appeals require ownership, can be submitted once per rejected case, and require the stronger moderation administrator permission to resolve.
Full capability reference
Rendered documentsartifact.renderingv0.5.3

Private documents your product’s registered renderers make on a durable job, downloaded until they expire and then deleted.

In the product
Users ask for a document and download it when it is ready. In Linklet, a collection’s QR codes print on one PDF, twelve to a page, that downloads for 24 hours.
Across the API
GET /artifacts/renderers lists what can be rendered and POST /artifacts asks for a document. GET /artifacts/{requestId} follows it, .../content downloads it and DELETE /artifacts/{requestId} removes it.
On the server
Runs only registered renderers with versioned parameter schemas, and its receipts keep a digest of the parameters, never the parameters or the document’s bytes. Access is checked when a document is asked for, again when it renders and at every download.

What it guarantees

  • A repeated request renders once
  • Downloads are uncached, sandboxed attachments
  • Expired documents are never served

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (4)
  • File storagestorage.object
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • Background jobsworkflow.jobs
Declared rules (13)
  • Only product-registered renderers execute and each invocation is product-authorized.
  • Every renderer names a versioned parameter schema and rejects input outside it.
  • Renderer metadata, parameters, output type, size, duration, concurrency, retention, and evidence are bounded.
  • Tenant and idempotency key identify one request; duplicate submission never renders twice.
  • Artifact bytes remain in host-owned private object storage; SQLite stores metadata and integrity evidence only.
  • Downloads re-authorize ownership, reject non-completed or expired artifacts, disable caching, and force sandboxed attachment handling.
  • Expired objects are deleted before receipts transition to purged; deletion is idempotent, one failing purge does not stop the others, and a purge that keeps failing is counted and waits behind work not yet attempted.
  • Raw parameters and renderer errors, credentials, source documents, and generated bytes never enter durable receipts.
  • A queued request renders on a durable job; duplicate jobs and repeated idempotency keys render it once.
  • Processing authorizes the recorded actor again and fails the request as renderer.unauthorized when access ended after it was requested.
  • Downloads authorize the recorded actor again; expired artifacts are unavailable whether or not the sweep has run.
  • A render that outlives its cancellation keeps its concurrency slot until the renderer returns or its grace ends, and bytes it writes after its request was abandoned, failed or erased are deleted, whether it then returns a result or an error.
  • Erasing an account queues the bytes of every request that may have them, including an unfinished or failed request's reserved reference, stops its renders still running, and asks the sweep to run at once.
Full capability reference
Explore Files and publishing

Privacy and data controls

Privacy & dataA record of the data the product holds, exports and erasure on request, and retention schedules with legal holds.3 capabilitiesData classificationData exportRetention and legal holdsInspect feature

A record of the data the product holds, exports and erasure on request, and retention schedules with legal holds.

API routes
11
data tables
4
migrations
7
declared rules
32

Feature source before its dependencies. Stack support is listed for each capability.

Data classificationdata.classificationv0.3.3

A vocabulary your product registers for its data, with an inventory of which resources hold each class. Retention, export and analytics decide from the same classes.

In the product
Users can read each kind of data the product keeps and how sensitive it is. Linklet’s handling page, above, is one way to show it.
Across the API
GET /data/classifications lists the vocabulary. GET /data/classified reads the inventory, and PUT and DELETE on /data/classified/{resourceKind}/{resourceId} classify a resource or remove its record.
On the server
Classifies a resource only when its kind’s resolver finds it among the account’s resources. A class may stay the same or become more restrictive; a downgrade needs an explicit product workflow.

What it guarantees

  • The inventory never stores payloads or credentials
  • Every read and write stays within one account
  • Unregistered kinds are refused

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (10)
  • Product source registers immutable versioned definitions before serving traffic.
  • Inventory stores bounded resource references and classification evidence, never resource payloads or credentials.
  • A resource is classified only when the resolver its kind registered finds it among the tenant's resources; an unregistered kind is refused.
  • The server warns at startup when the vocabulary defines classes but no resource kind is registered, because every classification is then refused.
  • Classification changes may remain equal or become more restrictive; downgrades, including removing a live resource's record, require an explicit product workflow through RemoveTx.
  • A record is removed through the operations only once its kind's resolver reports the resource gone.
  • Every inventory read and write is tenant-scoped.
  • Class digests make downstream retention, portability, and analytics decisions reproducible.
  • Definitions describe product policy; host domain services remain authoritative for enforcement.
  • The inventory reads a page at a time in resource order, which reclassifying never changes, so following the cursor reads every resource once.
Full capability reference
Data exportdata.portabilityv0.4.4

Idempotent export and erasure-plan requests, run by bounded handlers your product registers, with evidence that cannot be deleted.

In the product
Users export everything in one file and save it. The file downloads for 24 hours, then the server deletes it.
Across the API
POST /data/portability/requests starts an export or an erasure plan, GET /data/portability/requests/{requestId} follows it and .../artifact downloads the file. GET /data/portability/handlers lists what a request covers.
On the server
Runs each account’s idempotency key at most once. Handlers write private files and return references and checksums, and an erasure plan never deletes product data.

What it guarantees

  • Export contents never enter request tables
  • Request evidence cannot be deleted
  • Erasing an account expires its exports

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (4)
  • Data classificationdata.classification
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • File storagestorage.object
Declared rules (11)
  • Product source registers bounded handlers with stable versions, classification coverage, timeouts, and concurrency limits.
  • The same tenant and idempotency key executes at most once and returns the existing request on replay.
  • Handlers write private artifacts and return references and checksums; raw export payloads never enter portability tables.
  • The shared capability creates non-destructive erasure plans and never deletes product data; it deletes only the artifacts its handlers wrote.
  • Request state only moves pending to processing to completed or failed, and evidence cannot be deleted; an artifact's expiry only moves earlier and its purge is final.
  • Persisted failures use safe codes; provider errors and secrets remain in host-controlled logs only.
  • A completed artifact is served until its expiry, 24 hours unless the host sets PORTABILITY_ARTIFACT_RETENTION, and the capability's sweep then deletes its bytes; deleting bytes already gone succeeds.
  • Erasing an account expires every artifact of its requests in the erasing transaction, without object I/O, and the sweep deletes them; the requests stay as pseudonymous evidence.
  • Request creation and the erasure hook both lock the account row first, so a request created while its account is erased is either expired by the erasure or refused.
  • Each request's artifact reference is reserved and recorded before its handler runs; a failed or interrupted request's reserved artifact expires at its failure and the sweep deletes it.
  • An export of a class data.classification marks export prohibited is refused before a request exists; an erasure plan of it still runs.
Full capability reference
Retention and legal holdsdata.retentionv0.4.2

Versioned retention schedules, append-only assignments and legal holds. An executor carries out enforced policies and logs what it did.

In the product
Users can read how long each kind of data stays. In Linklet, each usage note is deleted 90 days after it is recorded.
Across the API
GET /data/retention reads the register and GET /data/retention/policies the schedules. PUT /data/retention/{resourceKind}/{resourceId} assigns one, POST .../holds places a legal hold and DELETE /data/retention/holds/{holdId} releases it.
On the server
Every assignment keeps the exact policy version and digest behind its date. An enforced policy runs across every account at its own interval, and each run is logged without naming whose records it touched.

What it guarantees

  • Hold evidence is never deleted
  • Releasing a hold is one way
  • The server refuses a policy it cannot honor

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Data classificationdata.classification
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (11)
  • Product source registers immutable versioned retention policies with stable classification references.
  • Every assignment is append-only and retains the exact policy version and digest used to calculate its date.
  • Only policies that explicitly allow legal holds may receive one.
  • Hold release is a one-way tenant-scoped transition and hold evidence is never deleted.
  • A policy registered with an enforcement is carried out by the executor across every account, at most its limit of records a run and at its own interval; any other policy's due plan is advisory, and product jobs authorize and execute its delete, anonymize, archive, or review actions.
  • An enforced policy cannot allow legal holds, because its enforcement acts on records in bulk.
  • Each enforcement run that acted on a record or failed is logged as evidence naming the exact policy version and digest, what carried it out, its limit and outcome, and how many records it acted on, never which ones or whose.
  • The server does not start when a policy names a class the classification vocabulary does not define, or when an enforcement's check finds it does not carry out what its policy promises.
  • A refusal names its cause: the invalid field, an unregistered policy, a class the policy does not cover, a resource with no assignment, a policy that allows no hold, or a hold that is not open.
  • Retention's owner operations act only on the caller's own account; reading the schedules and the register and placing a hold admit the person's session and a key or an assistant with data:read or data:write, assigning a schedule and releasing a hold admit the person's own session alone unless a product widens them, none is fixed, and an automation flow or a visitor is refused.
  • Retention records contain bounded resource references and reasons, never resource payloads or credentials.
Full capability reference
Explore Privacy and data controls

Developer API

Developer platformA documented public API with keys, allowances and a developer portal, and OAuth for the assistants people connect.4 capabilitiesDeveloper platformAgent surfaceAPI keysOAuth for assistantsInspect feature

A documented public API with keys, allowances and a developer portal, and OAuth for the assistants people connect.

API routes
23
data tables
10
migrations
6
declared rules
52

Feature source before its dependencies. Stack support is listed for each capability.

Developer platformapi.platformv0.3.6

The developer platform over the operation registry: the whole OpenAPI document, a plan allowance and a metadata-only log for keyed calls, signed pagination cursors and a developer portal.

In the product
Users find their API address, download the spec and copy real requests for their own workspace from a request workbench.
Across the API
GET /openapi.yaml serves the document. GET /developer/operations lists what a key may call and GET /developer/requests shows recent keyed calls.
On the server
Charges every keyed call to the plan’s API allowance before reading its parameters, and logs only metadata. Cursors are signed, bound to the account and query, and expire.

What it guarantees

  • Logs never keep credentials or payloads
  • A rolled-back call is neither charged nor logged
  • The portal lists only what a key may call

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Signing cursors needs API_CURSOR_KEY, which stays with the host.

Included dependencies (5)
  • Agent surfaceapi.agent-surface
  • API keysidentity.api-key
  • Usage and limitscommerce.metering
  • Observabilityoperations.observability
  • Secret referencessecurity.secret-references
Declared rules (6)
  • Operations are declared once in the operation registry; the OpenAPI document is generated from the same declarations the routes, tools and commands come from.
  • Every keyed call consumes the plan's API allowance before its parameters are read; a session is the product's own client and is not metered here.
  • A keyed call made inside an operation's transaction is charged and logged in that transaction, so a rollback leaves neither and the charge never waits for a second write connection.
  • The developer portal lists only operations whose callers, as the product set them, admit an API key; an operation only the person's own session runs never appears there.
  • Cursors are opaque, integrity-protected, tenant- and query-bound, and expire.
  • Request logs contain metadata only and never retain credentials or payloads.
Full capability reference
Agent surfaceapi.agent-surfacev0.8.9

One operation registry: each operation is declared once and served as a REST route with its contract, MCP tools, a command and a skill entry.

In the product
Scripts, assistants and the command line call the same operations the app does, under the same rules.
Across the API
Every operation appears in the OpenAPI document with typed parameters and results. Assistants reach the owner’s operations through the MCP server at /mcp.
On the server
Runs one pipeline on every surface: admission, rate limits, authorization, approval, metering and the handler, within the operation’s timeout. A declaration that breaks a rule stops startup.

What it guarantees

  • Nobody receives another caller’s replayed answer
  • An idempotent write runs once per key
  • A fixed operation is never widened

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (1)
  • Accounts and email sign-inidentity.human
Declared rules (24)
  • Every operation is declared once with typed parameters and results; its schemas are derived from the Go types.
  • A declaration that breaks a rule is refused at startup, every problem at once.
  • Every surface runs one pipeline: admission, rate limit, decode, the rate limit by parameters, authorize, approval, the rate limit by parameters again for an approver's edit, meter, handler, within the operation's timeout.
  • An operation that declares Actors admits only those kinds of caller, refused with actor_not_allowed at admission before its scopes on every surface, and code outside admission that refuses by kind answers the same problem through registry.ActorNotAllowed; one that declares none admits every kind its surfaces reach. Every owner operation should declare them, and a visitor operation declares none.
  • An operation declared on the flow surface admits automation. In a declaration and in a product's override alike, one that admits automation also admits owner.key and owner.agent or waits for the owner's approval; a product may still narrow a flow step past automation, which takes it out of its flows.
  • A fixed operation, one that guards a gate or is the person's own act, may be narrowed by a product and never widened past what it declared; widening one stops startup.
  • The owner's MCP server neither lists nor runs an operation whose effective Actors admit owner.session and neither owner.key nor owner.agent, for any caller: MCP is for keys and OAuth grants, and a session token is the person.
  • Admission runs before an idempotent replay, and replays are keyed by caller, so nobody receives another caller's recorded answer.
  • An idempotent write reserves its key before it runs, so one request runs per key across every replica; once the handler has run, its outcome, failure included, is what every retry is told for a day, except validation.failed, which means nothing changed and frees the key.
  • Deleting an account deletes its idempotency reservations and recorded outcomes in the deletion's transaction, so a recorded answer never outlives the account it was given to.
  • An operation marked DisclosesSecret answers a secret shown this once and its answer is never kept: REST refuses an Idempotency-Key on it before reserving anything, its contract declares no such header, and Define keeps it off the flow surface.
  • An approval-bound call answers pending without running, and the approval binds to a digest of its exact arguments.
  • An approved call spends its approval immediately before its handler, inside the operation's declared transaction on every surface, so the approval, the mutation, its audit and the recorded outcome commit together or not at all; a call that loses its approval to another caller is held anew without running.
  • A visitor operation declares the visitor surface and nothing else; the owner's MCP server never lists it and the visitor's never resolves an owner tool.
  • REST routes are declared resource paths in the API stack's contract style; a family's contract fragment is generated from its declarations and a test fails when the committed fragment drifts.
  • A named schema is one component however its uses differ, its uses learned across every family; the component carries only what every use agrees on, and a use that says more, its own description or that it may be null, says it beside the reference, in the fragment that defines the shape and in every fragment that shares it.
  • Structured MCP output is only the operation's result; a pending decision or a problem travels in _meta.
  • An operation's result metadata travels only in the MCP result's _meta, never in structured output or text, and counts toward its result bound.
  • A tool's visibility and _meta are hints for an MCP Apps host and never authority: every call passes the same admission, scopes, approval, rate limits and checks.
  • A modern MCP request carries its revision and client capabilities in _meta and headers that match its body, or it is refused before any method runs.
  • Every call is admitted against its operation's rate class before anything else about it is read; a visitor is limited by the network peer the transport saw.
  • The command line and the skill document are generated from the declarations like every other surface: a command exists only for an operation that declares cli, it calls the same REST routes, and the committed skill document fails a test when it drifts.
  • A bearer API key, when identity.api-key is composed, acts for its owner with only the key's scopes; a session holds every scope and is never handed to an assistant, so the command line reaches a session-only operation only on the API stacks with the person's own session token, and its login grant runs as owner.agent.
  • A session, key or access token that cannot be checked is logged with the route the request matched, never the request path, which can carry a secret.
Full capability reference
API keysidentity.api-keyv0.3.5

Keys a person owns for their scripts, with validated scopes, optional expiry, revocation and last-used tracking.

In the product
Users make a key with only the permissions a tool needs, see when each was last used and revoke it. The secret is shown once.
Across the API
GET /account/api-keys lists keys, POST /account/api-keys makes one and POST /account/api-keys/{keyId}/revoke revokes it.
On the server
Stores only a SHA-256 digest and a non-secret prefix, and authenticates with a hashed lookup that never compares plaintext. Each successful use records its time atomically.

What it guarantees

  • The secret is returned exactly once
  • Expired or revoked keys never authenticate
  • A key acts with exactly its scopes

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (1)
  • Accounts and email sign-inidentity.human
Declared rules (9)
  • Each credential contains 256 bits of cryptographic randomness and the raw secret is returned exactly once.
  • Only a SHA-256 digest and a non-secret display prefix are persisted.
  • Authentication performs a hashed indexed lookup and never scans or compares stored plaintext secrets.
  • Every key is owned by one product user and every management method takes that user ID first.
  • Scopes are validated, deduplicated, sorted, and stored as a canonical JSON string array.
  • Expired or revoked keys cannot authenticate.
  • Successful authentication atomically records last use.
  • Read and list results never contain the credential digest or raw secret.
  • On the API stacks only a signed-in session manages keys, and a key acts for its owner only through a capability that asks the verifier, with exactly the key's scopes.
Full capability reference
OAuth for assistantsidentity.oauth-providerv0.4.2

Your product as the OAuth 2.1 authorization server for the assistants users connect: PKCE, consent, audience-bound tokens with rotating refresh, and revocation.

In the product
Users approve an assistant where they are signed in, see their connected apps and disconnect one. The command line signs in with a code they approve.
Across the API
/.well-known/oauth-authorization-server describes the server. GET /oauth/authorize asks for consent, POST /oauth/token issues tokens and POST /oauth/revoke ends them. GET /account/connections lists connected apps.
On the server
Accepts only public clients that prove possession with S256 PKCE. Codes and tokens are stored as digests, and a replayed code or rotated refresh token ends the whole grant.

What it guarantees

  • A token works only for the resource it names
  • An assistant holds only its granted scopes
  • Implicit and password grants do not exist

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (13)
  • Clients are public: they prove possession with S256 PKCE, never a secret, and the implicit and password grants do not exist.
  • A client names a metadata document, fetched only from public addresses without redirects, within a size and time bound, or registered dynamically when that fallback is on.
  • A redirect URI matches a registered one exactly, except a loopback IP literal's port, and a refusal is sent to it only once it is trusted.
  • Authorization responses carry iss (RFC 9207).
  • An access token is bound to the one resource it was requested for and refused by every other.
  • Codes and tokens are stored as SHA-256 digests; a code is used once, and a replayed code or rotated refresh token ends the whole grant.
  • Consent happens where the owner is signed in, same-origin, and names the client's metadata host and the redirect host.
  • A grant acts as owner.agent with only its scopes, and a missing scope is answered with a step-up challenge.
  • The device grant serves the product's own command line only: its person approves the code on a signed-in page that warns against codes someone else sent, codes are tried a limited number of times, and an approval issues tokens once.
  • The hourly limits on registering a client and starting a device sign-in count each client address, on the API and Apple stacks the peer identity.human resolves behind its trusted proxy setting; the counts are per process.
  • Listing and disconnecting connected apps admit the owner's session, an API key and the product's own command line's grant, never another app's. Reviewing or answering a device code admits the owner's own session alone, a fixed declaration a product may narrow and never widen.
  • Once the operator lists redirect hosts, dynamic registration and every registered client keep only redirect URIs on a listed host or a loopback address, the product's own host included only when listed; metadata-document clients answer for their own.
  • The device operations review, accept and decline take a sign-in code only in the request body. No route carries it in its path. The browser device page carries it in the query string, as verification_uri_complete names it and as the page's code form submits it with method get, so proxies and access logs must not record that address's query string.
Full capability reference
Explore Developer API

In-chat apps

Developer platformYour product interface inside MCP Apps conversations, with scoped tools and private files delivered to the view.1 capabilityIn-chat app UIInspect feature

Your product interface inside MCP Apps conversations, with scoped tools and private files delivered to the view.

API routes
1
data tables
1
migrations
1
declared rules
8

Feature source before its dependencies. Stack support is listed for each capability.

In-chat app UIapi.app-uiv0.2.3

Views published as MCP Apps resources, with content-addressed identities, sandbox and CSP metadata, linked tools, scoped file grants and allowlisted host file inputs.

In the product
A compatible MCP Apps host renders your view beside the conversation. The local Linklet fixture can refresh the link, read click counts and display a private QR preview.
Across the API
MCP resources expose the view. Linked app-only tools use the same operation registry; short-lived file grants return only one authorized object.
On the server
Checks account ownership and scopes on each tool call and file fetch, limits host file inputs and refuses unlisted view origins. A result can keep private display metadata out of the model’s text.

What it guarantees

  • Views and tools keep the same ownership and scope checks
  • Private file URLs stay in result metadata
  • Revoked, expired and foreign-account grants are refused

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Host domains, sandbox origins and CSP must match the host you configure. ChatGPT, Claude and MCP Inspector have no recorded live qualification. The official SDK fixture proves the protocol integration only.

Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (8)
  • Views are self-contained HTML documents from the product's own source at ui:// URIs carrying a hash of the document and its metadata, so a changed view gets a new URI; Kore ships no host bridge.
  • Every view's sandbox and CSP metadata and the OpenAI compatibility keys come from the same settings; a list left empty grants nothing.
  • Tool visibility and in-chat metadata are host hints, never authority: an app-only operation keeps its actors, scopes, approval, rate limits and checks.
  • File grant URLs reach the view through result _meta only, never structuredContent or text, so the model never sees one.
  • Widget file delivery is the only signed access to private objects: a grant is 256 random bits, stored only as a hash, read-only, bound to one owner, object, file kind and Files view, expires within 15 minutes, can be revoked and is never logged; content.assets gains no general signed URLs.
  • The file route answers CORS only to configured view origins, never with credentials, and every refusal that could reveal a token is the same not found.
  • Host files are fetched only from configured exact https origins, resolving and pinning each public address, checking every redirect, with a byte bound and a timeout.
  • The capability claims the MCP Apps protocol only; an assistant host is supported only after a dated run on it is recorded.
Full capability reference
Explore In-chat apps

Integrations

Developer platformSigned webhooks in and out, versioned events with replay, and a Slack connection with encrypted credentials.5 capabilitiesDomain eventsInbound webhooksOutbound webhooksSlack connectorConnected credentialsInspect feature

Signed webhooks in and out, versioned events with replay, and a Slack connection with encrypted credentials.

API routes
16
data tables
9
migrations
11
declared rules
47

Feature source before its dependencies. Stack support is listed for each capability.

Domain eventsintegration.eventsv0.8.3

Versioned event schemas written to an outbox in the same transaction as the change they record, and delivered to consumers that each declare their event types, timeout and attempts.

In the product
Events work behind the screens. Linklet records one each time a link or collection changes, and owners and their keys read their own events over the API.
Across the API
GET /events lists the caller’s events newest first, in pages of up to 100, and a key needs events:read. POST /events/replay replays a subscription from a cursor, at most 500 events a call, to a consumer that allows it.
On the server
Leases each delivery and retries with bounded backoff, then parks exhausted work as a dead letter for operators. Replay queues a finished delivery again with a fresh attempt budget and never changes the original event.

What it guarantees

  • An event commits with the change it records
  • Order is kept within each aggregate
  • A replay cursor never crosses accounts

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (3)
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Background jobsworkflow.jobs
Declared rules (18)
  • Product state and its outbox event commit in the same database transaction.
  • Schemas are versioned, digested, classified, retained, and payload bounded.
  • Consumers declare accepted types, timeout, attempts, and idempotent behavior.
  • Leases outlast every consumer's timeout and recover after crashes; failures retry with bounded backoff, and exhausted work parks as a dead letter.
  • A consumer's panic fails only its delivery, recorded as consumer_panic without the panic's value; a delivery leased past its consumer's MaxAttempts without an outcome parks as attempts_exhausted without running again.
  • Parked deliveries are counted for operators: on the delivery page, and as the on-demand readiness check integration.events.deliveries on the API stacks.
  • Delivered events are pruned once their schema's retention passes; an event a registered consumer has not delivered, or that any consumer holds pending, processing or parked, is never pruned. An event committed before the recorded start of a consumer that starts at its registration needs no delivery to it.
  • A consumer that starts at its registration never receives an event of a type committed before its start for that type, recorded once in the database when the composition builds the delivery service (Service.RecordStarts), or by the first dispatcher that runs with it when nothing recorded it, and never moved; every event committed after it is delivered, however late. A consumer that already had deliveries of the type keeps receiving it from the beginning.
  • Lease-owner fencing prevents a stale replica from completing or retrying work after another replica reclaims it.
  • Replay re-queues a finished delivery with a fresh attempt budget, leaves a pending or processing one as it is, keeps the outcome it replaces and who asked in the event tenant's audit trail, and never mutates the original event.
  • events.replay replays a subscription from a cursor in commit order, at most 500 events a call, and records one domain_event.subscription.replayed entry per tenant it touched with the actor, the consumer and the cursor range. A cursor is an event identifier and never crosses tenants: one that names another account's event is refused.
  • Ordering is scoped to an aggregate rather than global.
  • Delivery evidence spans tenants, so it is operator-only: the verified accounts in the kernel's OPERATOR_EMAILS on the hypermedia and React stacks, the loopback operations routes on the API stacks. Replay repeats side effects, so an operator replays any consumer for one account or every account, and an owner replays only their own events, only to a consumer that declares OwnerReplay, and from a key or grant only with the events:replay scope. Whether a replay of another account comes from an operator stays the operation's own check.
  • An owner reads only their own events through events.list, newest first in pages of at most 100 that continue from the previous page's last event, which requires the events:read scope from a key or grant, with a confidential event's payload withheld; delivery evidence across tenants stays with the product's operators.
  • events.list and events.replay admit the owner's session, a key and an assistant's grant (Actors owner.session, owner.key, owner.agent), none of them fixed; a flow starts on events and is refused either operation by its kind with actor_not_allowed.
  • A sink sees every registered schema before the registry seals, so a consumer of every event type, such as the engine, names them all.
  • An event records its depth: zero when a person or the product records it, one more than the run whose step recorded it; a consumer runs under its event's depth (ADR 0045).
  • Erasing an account deletes the events whose tenant it is, with their deliveries and projections, in the deletion's transaction, and replaces the account's identifier as another tenant's event aggregate with a stable pseudonym. Deleting a team deletes the team's events the same way when collaboration.teams is composed. Payloads are never rewritten, so a source never puts personal data in another tenant's payload.
Full capability reference
Inbound webhooksintegration.webhook-inboundv0.3.4

A public receiver at POST /webhooks/inbound/{source} that checks an HMAC-SHA256 signature over the exact raw body before any product code runs.

In the product
A service your users rely on can post to your product, signed with a secret they hold. Linklet’s Link sender, above, is one way to show it: a CMS adds links with a secret Linklet shows once.
Across the API
Senders sign the timestamp, the event ID and the exact body, and send Webhook-ID, Webhook-Timestamp and Webhook-Signature. The same event sent again answers 204 with Webhook-Duplicate: true, and the same ID with different bytes is a conflict.
On the server
Reserves each source and event ID with a body digest before the handler runs, and runs handlers under timeout, panic and concurrency limits. Receipts keep state, hashes and error codes, never payloads or signatures.

What it guarantees

  • The raw bytes are verified before parsing
  • An event ID is handled once per source
  • Receipts are for operators only

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Nothing retries a failed event from your side: the sender’s next delivery of the same bytes is its retry, up to the attempt limit.

Included dependencies (3)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
  • Secret referencessecurity.secret-references
Declared rules (8)
  • Authenticate the exact raw JSON bytes before parsing or invoking product code.
  • Reserve each source and event id with a body digest before invoking a handler.
  • The same event id and body is idempotent; the same event id with different bytes is a replay conflict.
  • Secrets remain host-owned and handlers receive cloned payload bytes under timeout, panic, and concurrency containment.
  • Receipts retain state, hashes, and stable error codes, never payloads, signatures, or handler error text.
  • Receipts are operator evidence: the hypermedia and React page admits only the verified accounts in the kernel's OPERATOR_EMAILS, and on the API stacks the capability reads OPERATOR_EMAILS itself and its read-only receipts operations admit only those verified accounts' own sessions, telling anyone else the receipts do not exist; ingress stays public.
  • The receiver is a plain route on every stack because the sender's signature proves the caller; on the API stacks it answers the same statuses as problem documents and has its own contract fragment.
  • Nothing replays or retries a receipt from the product: the sender's next delivery of a failed event's same bytes is its retry, up to the attempt limit.
Full capability reference
Outbound webhooksintegration.webhook-outboundv0.2.7

Endpoints each account registers, with HMAC-SHA256 signed deliveries, retries through background jobs and a record of every attempt.

In the product
Users add an endpoint, keep its signing secret, send a test and pause or delete it. Each endpoint shows its recent deliveries and how each one ended.
Across the API
POST /webhooks/endpoints registers an endpoint and answers its secret once, and .../rotate-secret replaces it. GET /webhooks/endpoints/{endpointId}/deliveries lists deliveries, and .../test sends a webhook.test event with no account data.
On the server
Signs the timestamp, event ID and exact body, and refuses private, loopback and reserved addresses when it dials. Production requires HTTPS, redirects are refused and response bodies are discarded.

What it guarantees

  • Keys may stop delivery, never aim it
  • A secret is shown once, then its fingerprint
  • A canceled delivery never sends

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Production needs a 32-byte WEBHOOK_SECRET_ENCRYPTION_KEY, which stays with the host. Private addresses are allowed only in development.

Included dependencies (7)
  • Agent surfaceapi.agent-surface
  • Feature access by plancommerce.entitlements
  • Usage and limitscommerce.metering
  • Accounts and email sign-inidentity.human
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
  • Background jobsworkflow.jobs
Declared rules (11)
  • Every endpoint read and mutation is scoped by authenticated tenant identity first.
  • Production endpoint URLs use HTTPS and delivery-time dialing rejects private, loopback, link-local, multicast, and reserved addresses.
  • Signing secrets are generated randomly, answered once when made or rotated, and stored only as versioned AES-256-GCM ciphertext bound to the tenant and URL plus a SHA-256 fingerprint; moving an endpoint reseals the same secret, rotating replaces it at once.
  • Each request signs timestamp, event ID, and the exact body with HMAC-SHA256.
  • The webhook job handler is registered during capability setup before jobs workers start.
  • Delivery retries are owned by workflow.jobs on a dedicated queue and each attempted HTTP request records bounded evidence.
  • HTTP request duration, redirects, and response headers are bounded; raw response bodies are discarded.
  • Secret values never enter manifests, logs, job payloads, attempt evidence, or provenance records. On the API stacks create and rotate declare DisclosesSecret, so their answer is never recorded for a replay and an Idempotency-Key on either is refused before it runs.
  • Each event's delivery to an endpoint is pending, delivering, succeeded, failed, exhausted or canceled; disabling or deleting an endpoint cancels or removes what has not begun, and a canceled delivery never sends.
  • On the API stacks every endpoint and delivery operation declares its callers (ADR 0086): reads, disable and delete admit the owner's session, keys and assistants; registering, moving, enabling, testing and rotating admit the owner's session alone.
  • Erasing an account deletes its endpoints, sealed secrets, deliveries and attempt evidence on every stack.
Full capability reference
Slack connectorintegration.connector.slackv0.2.3

A connection to one Slack channel, with its bot token sealed as a connected credential, and a bounded sender your product calls with the messages it chooses.

In the product
Account settings on Apple and in the browser let owners connect, inspect, check and revoke a Slack connection. The pictured screens show webhooks; the Slack controls are a separate Account section. An uncertain check is never automatically replayed.
Across the API
API and Apple backends expose GET, PUT and DELETE /integrations/slack, with POST /integrations/slack/test for a deliberate check. Go web and React products also have settings forms. Connecting and revoking require recent authentication.
On the server
Posts only to Slack’s fixed chat.postMessage endpoint, refuses redirects and bounds time, message and response size. A lost or unreadable answer is reported as unknown, because Slack may have accepted the message.

What it guarantees

  • The bot token opens only inside the connector
  • Local mode never contacts Slack
  • Production refuses local mode

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Available on every declared starting point, including Apple and both API database profiles. Live mode needs a Slack app’s bot token. Local checks explicitly say that no Slack message was sent. A live Slack workspace has not been qualified by the local reference proofs.

Included dependencies (4)
  • Domain eventsintegration.events
  • Connected credentialssecurity.connected-credentials
  • Passkeys and two-step sign-inidentity.assurance
  • Permissions and custom rolessecurity.authorization
Declared rules (5)
  • Bot tokens are stored only through connected credentials and decrypted only inside the Slack connector.
  • Production uses the fixed Slack HTTPS endpoint, rejects redirects, and bounds time and response bytes.
  • Products own typed event mappings, exact destination review and durable send recovery.
  • Local mode performs no network access and is impossible in production.
  • Connection and revocation require recent authentication.
Full capability reference
Connected credentialssecurity.connected-credentialsv0.2.3

Envelope encryption for the tokens users grant your connectors: a random key for each record seals the token, and a host-owned key seals that key.

In the product
Users see each connected service, what it may do and when its access ends, and revoke any of them. Linklet’s Link sender, listed above with Create links, is one way to show it.
Across the API
GET /connected-credentials lists connections with their scopes, status and expiry, and POST /connected-credentials/{providerId}/revoke revokes one. Neither ever decrypts a credential.
On the server
Binds each ciphertext to its owner and provider, so it cannot move between accounts. Only connector code opens a credential, and revoking one removes its ciphertext and sealed key.

What it guarantees

  • Plaintext never reaches logs or screens
  • Ciphertext cannot move between accounts
  • Login tokens never become connections

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Production needs a 32-byte CONNECTED_CREDENTIALS_KEY, which stays with the host.

Included dependencies (4)
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Accounts and email sign-inidentity.human
  • Secret referencessecurity.secret-references
Declared rules (5)
  • A host-owned key encrypts a random per-record data key; the data key encrypts credential plaintext.
  • Owner and provider are authenticated additional data so ciphertext cannot move across boundaries.
  • Only connector code opens credentials and plaintext never enters plans, locks, receipts, logs, or generic UI.
  • Revocation removes ciphertext and encrypted data-key bytes.
  • Login-provider tokens are never promoted into connector credentials implicitly.
Full capability reference
Explore Integrations

App foundation

Developer platformWhat every product starts with: settings, typed configuration, logs and readiness checks, and secrets that stay with the host.6 capabilitiesSwift clientProduct settingsTyped configurationObservabilitySecret referencesProvider readinessInspect feature

What every product starts with: settings, typed configuration, logs and readiness checks, and secrets that stay with the host.

API routes
11
data tables
1
migrations
1
declared rules
26

Feature source before its dependencies. Stack support is listed for each capability.

Swift clientclient.corev0.9.4

The Swift client’s baseline: the server address, a Keychain session store and the transport every feature screen calls through. Every refusal arrives as one problem document.

In the product
Users sign in once and stay signed in to that server. Each server address keeps its own session, so a build pointed at another server never shares one. Linklet on iPhone takes its address from the build, not from a settings screen.
Across the API
It adds no routes. Each request carries Authorization: Bearer with the stored token, every non-2xx answer decodes as an RFC 9457 problem document, and a write can carry an Idempotency-Key.
On the server
Your server sees one opaque bearer token and no cookies. A 401 revokes the stored session, but only the credential that request sent, so a late refusal cannot sign out a newer login.

What it guarantees

  • Tokens stay in the Keychain, never in logs
  • A failed Keychain read is never “signed out”
  • The app switcher never pictures a secret

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite

It is the Swift client's baseline: server configuration, the Keychain session store and the transport. API profiles can add that native client explicitly. The legacy hypermedia and React backends retain their own browser baseline; native conversion for those backends is separate follow-up work.

Known limit. For the Swift app only. The web and React blueprints carry their own client baseline, and an API product ships no client.

Declared rules (4)
  • Sessions are bearer-only: the store holds one opaque token and the middleware sends it; no cookie is read or written.
  • The token lives in the keychain on device and in memory in tests and previews; it is never logged.
  • Every non-2xx response decodes to a problem document; a 401 revokes the stored session.
  • The package fragment pins the OpenAPI runtime and transport by lower bound; Package.resolved is the exact pin.
Full capability reference
Product settingsproduct.settingsv0.3.8

A catalog of the settings each capability contributes, each with a stable identity and a local destination. It records where settings live and holds no values or secrets.

In the product
Users search one list for the setting they need and go straight to it. The supplied iPhone screen keeps its last answer when a refresh fails and opens only destinations the app knows. Linklet’s clients do not show the catalog; its Settings and Account pages list their own sections.
Across the API
GET /settings lists the surfaces the caller may see, and ?q= searches them as literal text. Web stacks serve the same catalog as a page.
On the server
Validates and copies every contribution before publishing one deterministic snapshot. External URLs, control characters, duplicates and oversized metadata fail closed.

What it guarantees

  • Discovery never replaces authorization
  • Every destination is a local path
  • The catalog holds no values or secrets

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (1)
  • Accounts and email sign-inidentity.human
Declared rules (5)
  • Settings discovery never replaces route authorization or domain authorization.
  • Every surface has a stable capability identity and a local absolute destination.
  • Catalog construction validates and copies all mutable input before publishing a deterministic snapshot.
  • Each explicit contribution carries its source capability identity and an optional actor discovery authorizer.
  • A stack adapter may derive visible surfaces from its navigation model without making navigation the settings authority.
Full capability reference
Typed configurationproduct.configv0.4.3

Every configurable key is registered with a type, a default, whether it can change and an explanation. Users override values for their own account, and secret material is never configuration.

In the product
Users pick a value from its labeled choices and can return it to the default. Linklet’s default link lifetime, above, is one of its keys: new links last 7, 30 or 90 days, or never expire.
Across the API
GET /config reads the caller’s values, PUT /config/{key} sets one and DELETE /config/{key} returns it to the default. Web stacks add a page at /settings/config.
On the server
Refuses unknown, malformed and immutable writes with a named code. A stored value that no longer fits reads as the default, is logged once and fails an on-demand readiness check.

What it guarantees

  • Overrides never cross owners
  • Answers redact every sensitive value
  • Keys and assistants may read, not write

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (5)
  • Every key is registered with one type, default, mutability rule, and human explanation before values are read.
  • Unknown, malformed, or immutable writes fail closed at the service boundary.
  • Tenant overrides never cross owner boundaries and diagnostics redact values marked sensitive.
  • The operations act only on the caller's own account; config.snapshot admits the person's session and a key or an assistant with config:read, config.set and config.reset admit the person's own session alone unless a product widens them, none is fixed, and an automation flow or a visitor is refused.
  • Secret material is excluded; configuration may contain references or non-secret selectors only.
Full capability reference
Observabilityoperations.observabilityv0.2.2

A fresh request ID on every response, one log line per request, and readiness checks a deploy can wait on.

In the product
Every response carries an X-Request-ID header that an operator matches to one log line. Neither the browser nor the iPhone has a screen for it.
Across the API
GET /readyz gives a deploy the status and checks, with 503 and Retry-After while a check fails. GET /ops/readiness gives an operator on the same machine the full document, with reasons, secrets and providers.
On the server
Logs the method, matched route pattern and duration, never query strings, headers, bodies or credentials. Incoming request IDs are not trusted, liveness checks no dependencies, and readiness is time-bounded.

What it guarantees

  • Every response gets a fresh 128-bit ID
  • Logs never keep request content
  • Readiness answers within its bound

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Raw paths are logged only when development allows them, and the full readiness document answers only on loopback.

Declared rules (6)
  • Every HTTP response receives a fresh cryptographically random request ID.
  • Request logs name the matched route pattern, never the raw path, unless development explicitly allows raw paths.
  • Request logs exclude query strings, headers, bodies, credentials, and response content.
  • Liveness reports process availability without checking dependencies.
  • Readiness is bounded and reports whether required local dependencies can serve work.
  • Telemetry failure never silently weakens request identity generation.
Full capability reference
Secret referencessecurity.secret-referencesv0.2.2

A registry of the secrets each capability needs, by reference name, owner, purpose, rotation policy and reload strategy. Values never enter it.

In the product
On web stacks, /settings/secrets lists each reference as configured, missing, expiring, invalid or unknown. Linklet runs the API profile, so neither client has a page; its references appear in the operator’s readiness document.
Across the API
GET /ops/readiness lists each reference’s name, owner, whether it is required and its state, and the on-demand check security.secret-readiness fails when a required reference is missing or invalid.
On the server
Runs at most eight checks at once, each with a time bound, and a timeout or panic reads as unknown. The host’s configuration decides presence and builds the provider clients.

What it guarantees

  • Values never reach logs, views or receipts
  • A stuck check reads as unknown
  • Rotation stays with the host

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. It is not a secret store. Values stay in the host’s configuration, and rotation is the host’s job.

Declared rules (3)
  • Descriptors contain reference names and lifecycle metadata only; secret values never enter registry state, snapshots, logs, receipts, or views.
  • The host configuration boundary determines presence and constructs provider clients; this capability is not a secret store.
  • Checks are time-bounded, panic-contained, concurrently bounded, and return sanitized operator messages.
Full capability reference
Provider readinessprovider.readinessv0.2.2

One probe for each external provider, such as mail, billing or sign-in, reporting ready, degraded, unavailable, misconfigured or unknown.

In the product
On web stacks, operators see every provider’s state at /settings/providers. Linklet has no page for it on either client; providers appear in the operator’s readiness document.
Across the API
GET /ops/readiness lists each provider with its owner, state and reason, and any provider that is not ready marks the whole document degraded.
On the server
Probes share eight slots, honor the request’s deadline and contain panics, and their reasons are sanitized. Registration is deterministic and refuses duplicates.

What it guarantees

  • Credentials appear only by name
  • Each provider has exactly one probe
  • Quota data never grants access

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. API stacks and Apple’s server have no page; providers report through the readiness document. Probes never inject secrets.

Declared rules (3)
  • Credential references are names only; secret values never enter descriptors, snapshots, logs, or views.
  • Provider probes are bounded, context-aware, panic-contained, and return sanitized diagnostics.
  • Registration is deterministic and rejects duplicate provider identities.
Full capability reference
Explore App foundation

Jobs and workflows

Automation & AIBackground jobs, schedules in the owner's time zone, approvals of exact actions, and automations owners build themselves.4 capabilitiesBackground jobsSchedulesApprovalsAutomationsInspect feature

Background jobs, schedules in the owner's time zone, approvals of exact actions, and automations owners build themselves.

API routes
8
data tables
17
migrations
24
declared rules
72

Feature source before its dependencies. Stack support is listed for each capability.

Background jobsworkflow.jobsv0.4.5

A durable queue in your database. Workers claim jobs under a lease, retry failures across a bounded window and set aside work that cannot succeed as dead letters.

In the product
Users never manage jobs; jobs carry the work they start. In Linklet, the weekly link report and every automation run go through them.
Across the API
GET /jobs lists the newest 100 jobs, by status and queue, for verified operators named in OPERATOR_EMAILS. It never shows a job’s payload or idempotency key, and a key, an assistant or a flow is refused.
On the server
A product inserts its change and the job it causes in one transaction. A claim counts one attempt, retries spread across an hour by default, and a permanent failure or an unknown kind goes to the dead letters.

What it guarantees

  • Only the lease holder settles a job
  • An idempotency key keeps one job per queue
  • Erasing an account deletes its jobs

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Jobs have no page on any stack; operators read them only through the API.

Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (18)
  • A claim is one atomic conditional update and increments attempts exactly once.
  • Expired running leases are reclaimable after process failure.
  • An attempt whose lease expires unsettled counts as failed: the job waits on the retry schedule from the lease's end with a lease_expired last error, and is not charged a further attempt until it is claimed again.
  • An expired lease at max attempts is dead-lettered with a lease_expired last error rather than reclaimed.
  • Completion, retry, and dead-letter transitions require the current lease owner.
  • Idempotency keys deduplicate within a queue without changing the original job.
  • A dead job enqueued again with the same key and inputs runs again with fresh attempts; different inputs under the key are refused.
  • A job's retries are spread across a bounded retry window, an hour by default, each delay between the retry base and maximum.
  • Domain state and its resulting job can be inserted through one store-owned SQL transaction.
  • Unknown job kinds are dead-lettered rather than retried forever.
  • Handlers honor cancellation and the configured attempt timeout.
  • A queue's JOBS_QUEUE_SETTINGS entry sets the lease of its claims, its handler timeout and its worker count, each unset one inheriting the service value; the settings are per queue because a claim sets the lease before the kind is known, and an entry for a queue the process does not serve or outside the service's bounds stops startup.
  • Permanent failures dead-letter immediately and retry-at outcomes remain bounded by max attempts.
  • Worker shutdown is bounded and finishes before database handles close.
  • A job enqueued with an owner is deleted, whatever its state and with its payload, in the transaction that erases that account; a job without one is product work and outlives every erasure.
  • An idempotency key never moves a job to another owner.
  • The workers delete succeeded and dead jobs after their bounded retention, at least 48h and at most 2160h, in bounded batches of single-row deletes that never touch a queued, running or retrying job; a swept job's idempotency key is free again.
  • jobs.list declares Actors [owner.session], so a key, an assistant or a flow gets 403 actor_not_allowed; whatever kinds a product admits, it answers only a verified account OPERATOR_EMAILS lists and never shows a job's payload or idempotency key.
Full capability reference
Schedulesworkflow.schedulingv0.5.5

One-time, interval and weekly schedules for targets your product registers. Each occurrence enqueues one background job.

In the product
Owners pick what to run, the days and a local time in their own time zone, and see the next run in their device’s time zone. They stop a schedule or delete it.
Across the API
GET /schedules/targets lists what can be scheduled and POST /schedules makes a schedule. POST /schedules/{scheduleId}/disable and .../enable stop and resume it, and DELETE /schedules/{scheduleId} removes it for good.
On the server
Advances each occurrence and enqueues its job in one transaction. After downtime a repeating schedule runs once and moves to its next future time. Each owner keeps at most 50 schedules enabled, and an interval is at least five minutes.

What it guarantees

  • Clients never choose a job kind or payload
  • A repeated local time runs once
  • Deleting leaves an enqueued run to finish

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. No monthly or other calendar rules, and no host cron. Time zones come from the host’s zone database, or the copy built into the server when the host has none.

Included dependencies (3)
  • Background jobsworkflow.jobs
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (9)
  • Only trusted product-registered targets can be scheduled; clients never choose arbitrary job kinds or payloads.
  • Advancing an occurrence and enqueueing its idempotent durable job commit in one transaction.
  • One-shot schedules disable after enqueue, and interval and weekly schedules skip missed runs to one future occurrence.
  • Store validation and durable schema triggers enforce cadence and interval consistency.
  • Tenant schedule counts, interval bounds, due batches, worker lifecycle, and shutdown are bounded.
  • A weekly schedule runs on its days at its time in its IANA zone across daylight saving changes; a skipped time runs as the clock passes it and a repeated time runs once.
  • A schedule enabled again resumes at its next occurrence after now, never replaying the runs it missed while disabled; a one-time schedule that has run cannot be enabled, and enabling counts against the owner's enabled-schedule limit.
  • Only a schedule's owner disables, enables or deletes it; deleting removes the schedule for good and leaves a run it already enqueued to finish.
  • The schedule operations admit the owner's session, keys and assistants and no other kind of caller; a key or an assistant reads with schedules:read and changes with schedules:write.
Full capability reference
Approvalsworkflow.approvalsv0.7.7

Owner approvals of exact operation calls. When a key, an assistant or a flow calls a guarded operation, the call waits, and the owner reads the operation’s own preview before deciding.

In the product
Owners read what waits, who asked and the preview, then approve or deny, and a signing key on their device can sign each answer. Linklet’s Waiting for you section, above, is one way to show it.
Across the API
GET /approvals lists what waits and GET /approvals/{approvalId} reads one. Only a person’s own session answers, through POST /approvals/{approvalId}/challenge and .../decision. A call that waits answers 202 with its approval ID.
On the server
Seals the preview with the approval and records it by digest; no push or mail carries it. An approval waits between five minutes and seven days, a long wait reminds once when a quarter of it remains, and its state only moves forward.

What it guarantees

  • One approval allows one execution
  • A changed call needs a new approval
  • Decisions cannot be edited or deleted

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL
Included dependencies (5)
  • Background jobsworkflow.jobs
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Notification routingcommunication.routing
  • Accounts and email sign-inidentity.human
Declared rules (19)
  • Trusted product source registers every approval policy and whether self-approval is permitted.
  • Only the assigned approver may transition one pending request to one final outcome.
  • Decision rows are append-only and protected against update and deletion by durable schema triggers.
  • Final request state is immutable and every decision insert must match the assigned approver and durable outcome.
  • Request state, audit evidence, and idempotent notification enqueue commit in one tenant-scoped transaction.
  • An operation approval binds one call: the owner, the operation, the digest of its parameters, the actor's kind and client, and the call site; it grants one execution and is consumed by it, in the transaction the call's work commits in when the operation declares one, so work that fails or never commits leaves it unspent and callers racing for one approval run the call once.
  • One decision covers a sealed batch of exact calls, each bound by its own digest and each granted one execution; the gate's own question is a batch of one.
  • An approval waits for its operation's declared lifetime when it has one, else the product's default, and never less than five minutes or more than seven days.
  • Under the plain owner rule an owner's session runs an approval-bound operation it calls directly at once; a call a model proposed in that session, and every call of a key, an assistant or a flow, waits for the owner's decision, which only an owner's session makes, and the approval shows the proposed call.
  • An operation that declares a named policy waits for that policy's reviewers, the owner's session included: trusted product source registers the policy, a directory resolves its group within the owner's tenant, the reviewers are recorded when the approval is asked and must still be resolved when they answer, and no caller names a policy or a reviewer.
  • A policy's approval is approved when its quorum of distinct reviewers approve and rejected at the first rejection; each reviewer answers once, which the database enforces, and fewer reviewers than the quorum refuses the request.
  • An approver reads the operation's own preview of the call, rendered from its validated parameters, bounded, sealed with the approval and recorded by digest; no caller writes it, and no push or mail carries it.
  • A pending approval that waits an hour or more reminds whoever still owes an answer once, when a quarter of its lifetime remains; a policy's escalation group, resolved when the approval is asked, may answer toward the same quorum only once its delay passes, which the database enforces; reminder and escalation times are sealed with the approval and no caller sets them.
  • An approver edits only the top-level string fields an operation declares, within their declared lengths, and only by approving: the edit closes the approval it edits and makes a new approval bound to the edited call's digest in the same batch, at most ten times per call, clears every earlier review and is recorded by field names and digests; the gate authorizes the actor for the edited call again and runs it in place of the call asked for, once, and the caller and listeners keep the identifier first named. No key, assistant, flow or model edits.
  • An answer to a batch that was edited carries the batch digest its answerer read, and one naming a replaced approval or a digest that no longer matches is refused.
  • A device key signs only what the server issued for one purpose: a decision's challenge names the signer, the key, the approval, its batch and batch digest, the outcome, and the digests of the reason and edits, expires in five minutes and is spent once, in the transaction that records the answer. A signature never replaces the signed-in session that sends it; a named policy, or an operation's owner policy, may require one, sealed on each approval asked under it and alike across its batch, and the database refuses a decision or a review that does not name its answerer's signature. Only a session that signed in within ten minutes registers a key, proved by the key's signature; rotation is signed by the old key and the new; any signed-in session revokes a key as removed or lost; each change is audited and told to the person by mail, and a retired key signs nothing more while what it signed stays valid. No key, assistant, flow or model registers, holds or uses a device key.
  • Every operation declares its callers: the owner's session, keys and assistants read what waits, a key or an assistant only with approvals:read, and a flow reads nothing; answering, its challenge and the five device-key operations admit only the person's own session and are fixed, so admission refuses every other kind with actor_not_allowed and no product widens them.
  • Approvals expire, pending or approved and unused, and states only move forward, which the database enforces.
  • Creating an approval routes the owner's notice and schedules its expiry, reminder and escalation jobs, asking enqueues each listener's request job for every call it asks about, and a decision or expiry enqueues its decided jobs, in the same transaction; an expiry is routed to the approval's deciders and its owner once per batch.
Full capability reference
Automationsworkflow.automationv0.17.10

An owner’s flows as versioned data: a trigger and up to 24 steps of actions, conditions, delays, waits and branches, published against your product’s operations and run through background jobs.

In the product
Owners start from a template, publish a version, turn it on and run it, then read each step’s status and attempts. Publishing and turning a flow on are separate decisions.
Across the API
GET /flows/catalog lists the templates and POST /flows saves a draft from one. POST /flows/{flowId}/publish and .../enable wait for the owner’s approval when a key or an assistant calls them, POST /flows/{flowId}/runs starts a run and GET /runs follows them.
On the server
Checks each version before it publishes and runs each step as a job. A delay or wait lasts at most 30 days and holds no lease, and a finished run is deleted 30 days after it ends.

What it guarantees

  • A run starts once per trigger key
  • Runs and steps only move forward
  • Audit receipts hold digests, never parameters

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. It requires domain events and feature access by plan beside jobs, schedules and approvals.

Included dependencies (9)
  • Background jobsworkflow.jobs
  • Schedulesworkflow.scheduling
  • Approvalsworkflow.approvals
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Feature access by plancommerce.entitlements
  • Usage and limitscommerce.metering
  • Accounts and email sign-inidentity.human
  • Domain eventsintegration.events
Declared rules (26)
  • A flow's action calls only a registry operation that declares the flow surface, as the automation actor, under every scope check, rate class and authorization.
  • A version is granted its actions' scopes once, when first published; a publisher cannot grant a scope it does not hold.
  • A run carries the version it started with and starts at most once per trigger key.
  • A schedule's occurrence starts only its owner's published, enabled flows whose trigger names that schedule, once per occurrence; publishing accepts only the owner's schedule for flows, and on.
  • Every transition is one transaction that checks the state it expects, with the next step's job; runs and steps only move forward, which the database enforces.
  • Delays and waits are jobs with later starts, at most 30 days, and never hold a lease.
  • Flows are bounded: 24 steps, nesting depth 2, 256 KB of definition, trigger payload and context, 64 KB per result.
  • An action whose operation needs approval waits for the owner's decision on that exact call, which only the decision resumes; approved, it runs once; rejected or expired, the run fails with the approval's code.
  • A finished run is deleted with its steps and waits 30 days after it ends; flows and their versions are kept.
  • The engine consumes only event types a capability or the product contributed to flows, each of which the product must record; an event starts only its tenant's published, enabled flows whose trigger names its type, once per event; a confidential event's payload never reaches a run.
  • A run's start, each action call and its end are receipts on the owner's audit chain in the transaction that changes the run, holding digests, codes and names, never parameters or results.
  • Publishing grants the scopes of every operation an action calls for the flow, which the publisher must hold.
  • A run keeps its event's depth and its steps record events one deeper; an event at depth five or more starts no flow, and each refusal is recorded on the owner's chain (ADR 0045).
  • At most a flow's concurrency of its runs work at once; a run waiting for a key, an approval or a delay's end gives its slot back, and a step that finds every slot taken is queued and granted the next one, oldest first, in the transaction that frees it (ADR 0046).
  • A test is a real run from a synthetic trigger of its flow's kind, keyed test: and marked test on its trigger, run and start receipt.
  • A retry is a new run of the published version from a failed or canceled run's trigger, once per run, naming it in retry_of; the run it retries is never changed.
  • A template's blanks are the only way an owner's values enter its definition, a value in an expression is one literal, and a version keeps the template and values it was filled from (ADR 0047).
  • A template naming an entitlement feature is filled only by an account the composed entitlement service grants it, checked on every path that fills one.
  • A gated template is refused when no entitlement service is composed, never allowed.
  • An ungranted template is listed rather than hidden, so a builder can show what a plan would unlock.
  • An offered blank takes only a value offered to the account filling it, checked when it is filled: its own schedule for flows that is on, an offered event type, or a flow-surface operation taking no parameters that admits a flow and the caller's kind and whose scopes it holds; only an offered blank fills an action's operation.
  • The engine ships a template for each trigger it owns, the event one only when an event type is offered, unless a contribution omits them.
  • An action's keep projection stores only the parts of its answer it names, before the result bound and the next step apply; a flow's calls carry its owner's tenant.
  • Every flows and runs operation admits the owner's session, an API key and an assistant and never a flow; a key's or an assistant's publish or turning a flow on waits for the owner's approval, the session's runs at once, and turning a flow off is a withdrawal any of them makes at once (ADR 0086).
  • Publishing refuses a step, or an operation a step calls for the flow, that does not admit automation or the publisher's kind, so a flow never calls what its publisher could not; the catalog and offered actions leave out what a flow may not call.
  • Only the owner's own session tests a flow that is off or retries a run someone canceled.
Full capability reference
Explore Jobs and workflows

AI features

Automation & AIModel access across providers, tools a model may call within a budget, and evaluations that catch regressions.3 capabilitiesAI model providersAI tools and budgetsAI evaluationsInspect feature

Model access across providers, tools a model may call within a budget, and evaluations that catch regressions.

API routes
7
data tables
4
migrations
3
declared rules
39

Feature source before its dependencies. Stack support is listed for each capability.

AI model providersai.providersv0.3.4

Generation through one interface for OpenAI, Anthropic, Gemini, OpenAI-compatible APIs and a deterministic local provider. The host constructs one provider at startup and owns its credentials.

In the product
Users can see which provider and model answer before they ask. Linklet names them beside its Describe check and in the app’s AI settings, which is one way to show it.
Across the API
GET /ai/provider describes the configured provider and model to a signed-in caller without calling it. Other capabilities generate through the provider rather than a route of their own.
On the server
Validates and bounds every request, tool schema, response and stream. External endpoints must use HTTPS, redirects are not followed, provider failures are sanitized, and no adapter retries a billable request on its own.

What it guarantees

  • Requests never carry credential material
  • Production refuses the local provider
  • Readiness never spends tokens

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. A real model needs AI_MODEL and its provider’s key: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, or AI_API_KEY with AI_BASE_URL for a compatible API. Keys stay with the host. Remote providers are not yet sent a response schema, so the caller checks every answer.

Included dependencies (2)
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (7)
  • The host owns credentials and constructs exactly one provider at the composition root; requests never contain credential material.
  • Requests, tool schemas, tool histories, responses, streams, timeouts, and endpoints are validated and bounded.
  • Request validation never mutates caller-owned slices; malformed or incomplete provider tool calls fail closed instead of becoming an apparent final response.
  • External endpoints require HTTPS, redirects are not followed, provider failures are sanitized, and no adapter retries a billable request implicitly.
  • External models are explicit configuration, while the deterministic local provider is rejected in production; it answers with a fixed reply, a quoted tool result, or a value meeting the request's response schema, never an echo of the request, and calls only an offered tool the latest user turn names, with well-formed arguments.
  • Opening readiness or settings pages never invokes a model or spends tokens.
  • A response schema is one bounded JSON object the caller still checks the answer against; providers never treat it as a guarantee.
Full capability reference
AI tools and budgetsai.toolsv0.6.5

Pinned prompt versions and product-owned tools a model runs through, inside a hard monthly token budget. Each generation costs one AI step, and a tool that needs approval waits for the owner’s yes.

In the product
Users see each AI request made on their device with its model and tokens, and anything waiting for their yes. Linklet’s Describe, which suggests a name and tags for a link, is one way to ask.
Across the API
POST /ai/generate runs a registered prompt version as the caller, within the caller’s budget. A call names its version, and a missing version is refused, never replaced by another.
On the server
Reserves one AI step and the most tokens a call can use before the model is asked, then settles to what the provider reports. All calls in one model response are checked before any runs, and an answer must meet its prompt’s output schema after at most one repair.

What it guarantees

  • An unlimited AI allowance is refused
  • Approval-required tools never run from model output alone
  • Evidence holds digests, never the input or answer

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. A model is never offered a destructive operation: ai.generate refuses a destructive tool in a prompt’s list.

Included dependencies (5)
  • AI model providersai.providers
  • Agent surfaceapi.agent-surface
  • Audit trailaudit.trail
  • Usage and limitscommerce.metering
  • Approvalsworkflow.approvals
Declared rules (21)
  • The registry requires an explicit authorizer, effect classification, JSON object schema, handler, timeout, and result bound for every tool.
  • All calls in one model response are preflighted before any handler runs; approval-pending or denied batches produce no partial side effects.
  • Read-only calls may run with bounded concurrency while write and external calls preserve provider order.
  • Tool validators, authorizers, handlers, observers, rounds, calls, arguments, results, and concurrency are bounded and panic-contained.
  • Actor identities and scopes are bounded, and caller-owned requests, scopes, and tool arguments are defensively copied across callback boundaries.
  • Handler and authorization errors are never sent to the model or rendered; safe failure results preserve the loop without leaking details.
  • Approval-required tools stop as pending work and are never executed from model output alone.
  • A prompt version is registered once and never changes; a call names its version, and a missing version is refused, never replaced by another (ADR 0044).
  • ai.generate admits the owner's session, keys, assistants and flows (owner.session, owner.key, owner.agent and automation) and refuses any other kind with actor_not_allowed; a product may narrow the list, and one that keeps automation keeps owner.key and owner.agent (ADR 0086).
  • A model receives a call's input unchanged as the user message and reads it as data, never as instruction, which the engine says in the system instruction rather than around the input; it may call only the operations its prompt version lists, through the registry as the caller, and a destructive operation cannot be fenced.
  • Every provider call reserves its most on ai.tokens and settles to what the provider reports; an unlimited AI allowance is refused.
  • An answer meets its prompt's output schema after at most one repair, and its evidence holds digests, never the input or the answer.
  • A model asking for an operation that needs approval runs nothing: the whole proposed batch goes to the owner as one decision, each call bound by its own parameter digest, in the owner's own session too, and the approved calls run marked as proposed so the gate holds them for that yes; a call the owner makes directly is unchanged.
  • A held proposal is resumed, never asked of the model again, and the approved calls run once each; a recorded call that no longer digests to what was approved runs none of them.
  • A spent decision is never reused: once the approved calls have run, the same question is a fresh question needing a new decision.
  • A generation costs exactly one AI step, reserved before the model is asked and released when the generation does not happen.
  • A refusal names which ceiling stopped the generation: the plan's step allowance, the owner's own spending limit, or a plan that grants no steps.
  • Resuming a held proposal is not a new generation and costs no step.
  • A proposal commits with the batch it is bound to or not at all, is keyed by the client that held it, and is resumed only by that client.
  • A call the caller may not make ends the generation and proposes nothing, even beside calls that only needed approval; rejected arguments go back to the model as a failed result and nothing in that turn runs.
  • A replay re-runs a prompt's recording in the model's place: each recorded call goes through the caller's fence and its operation's own authorization again, its recorded result standing in for running it, and the recorded answer through the output check; a call the fence now refuses, or another answer, diverges. It asks no model and spends, holds, runs and records nothing.
Full capability reference
AI evaluationsai.evaluationsv0.2.4

Versioned suites of cases and assertions, scored by deterministic checks, with each run compared against the one before. A changed suite starts a fresh comparison.

In the product
Users start a run and read its score, each case’s checks and answer, and how it compares with the last run. Linklet’s Describe check, above, is one way to show it.
Across the API
GET /ai/evaluations/suites lists suites, and GET /ai/evaluations/runs and GET /ai/evaluations/runs/{runId} read the history. POST /ai/evaluations/runs starts a run and needs an Idempotency-Key.
On the server
Bounds each run’s cases, concurrency, time per case, output tokens and kept evidence. A run is compared only with the owner’s previous run of the same suite, version and digest, and fails on a score loss beyond tolerance or a case that passed before.

What it guarantees

  • Reading results never calls a model
  • A retried key answers the recorded run
  • The newest 10 runs of each suite are kept

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Scorers are deterministic: exact text, contained text, valid JSON, non-empty text and named tool calls. Grading with another model needs a separate capability, and this one does not meter what a run spends.

Included dependencies (3)
  • AI model providersai.providers
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (11)
  • Products register immutable, versioned suites at the composition root; registry snapshots and request payloads are defensively copied.
  • Suite digests cover ordered cases, prompts, model controls, tools, tags, assertions, weights, and thresholds.
  • Only deterministic built-in scorers run in the reusable layer; arbitrary judge callbacks and model-graded assertions require a separate capability.
  • Runs bound case count, concurrency, per-case time, requested output tokens, and retained output bytes; results retain suite digest, provider identity, model identity, scores, usage, and ordered case evidence.
  • Provider failures are observable to trusted host code but are sanitized in evidence and UI output.
  • Regression comparisons reject suite digest drift and fail on score loss beyond tolerance or any previously passing case that now fails.
  • Opening the evaluations page, listing suites or reading runs never invokes a model; only an explicit authenticated POST or ai.evaluations.runs.start starts a potentially billable run.
  • Each completed run is kept in its owner's history with every case's result; the newest 10 runs of each suite are kept per owner, and an interrupted run records nothing.
  • A run is compared only with the owner's previous run of the same suite ID, version and digest; a changed suite reads as suite_changed and every case as new.
  • ai.evaluations.suites.list, runs.list and runs.get admit owner.session, owner.key and owner.agent; ai.evaluations.runs.start admits only owner.session, as the web page does, and refuses any other kind with actor_not_allowed (ADR 0086).
  • ai.evaluations.runs.start requires an Idempotency-Key: a retried key answers the recorded outcome without calling the provider again, and a refusal before the run spends nothing.
Full capability reference
Explore AI features

Operations and insight

Operations & insightMetrics, availability objectives, incidents and a public status page, operator checks, and consent-aware product analytics.3 capabilitiesReliability and statusOperator checksProduct analyticsInspect feature

Metrics, availability objectives, incidents and a public status page, operator checks, and consent-aware product analytics.

API routes
22
data tables
6
migrations
9
declared rules
32

Feature source before its dependencies. Stack support is listed for each capability.

Reliability and statusoperations.reliabilityv0.3.3

Bounded metrics with W3C trace propagation and export, an availability objective with burn rates, team incidents with an append-only timeline, and a public status page.

In the product
Team owners and admins are alerted when an incident opens and each time its state changes, on the channels they chose for incident alerts. Anyone can read a team’s public status without an account.
Across the API
GET and POST /teams/{teamId}/incidents list and open incidents, and POST /teams/{teamId}/incidents/{incidentId}/transition moves one. GET /status/{teamSlug} is the public status, and GET /ops/reliability gives the host its operator snapshot.
On the server
Measures HTTP availability against 99.90 percent over 5-minute, 1-hour and 30-day windows, and reads too little traffic as unknown. Only a team’s owners and admins change its incidents, and every change appends to the timeline in the same transaction. Metric labels come from fixed lists, and the total number of series is capped.

What it guarantees

  • Resolved incidents never reopen
  • The status page shows only published incidents
  • An exporter never holds up a request

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. On the API and Apple stacks there is no incident page and the public status is JSON; the status page and reliability settings come with the web stacks. Exporting to an OTLP collector can use OTEL_EXPORTER_OTLP_TOKEN, which stays with the host.

Included dependencies (7)
  • Agent surfaceapi.agent-surface
  • Teams and rolescollaboration.teams
  • In-app noticescommunication.messaging
  • Notification choicescommunication.preferences
  • Observabilityoperations.observability
  • Provider readinessprovider.readiness
  • Secret referencessecurity.secret-references
Declared rules (12)
  • Metric labels are definition-owned allowlists and total series are bounded.
  • Trace context uses W3C traceparent while request correlation remains a separate identity.
  • Exporter queues never block request handling and expose drops and failures.
  • OTLP credentials are host-owned and never stored with telemetry or incident evidence.
  • SLO evaluation declares its missing-traffic policy and is deterministic for the same inputs.
  • Incident mutation is tenant-scoped to workspace owners and admins.
  • Incident timelines are append-only and resolved incidents cannot reopen.
  • Timeline evidence names its actor by an opaque identifier, so erasing an account keeps the evidence it wrote.
  • A timeline entry is deleted only with its incident, and an incident only once it is resolved and past its retention or when its team is deleted.
  • Public status contains only explicitly public, product-safe fields.
  • Alert delivery is idempotent and delivery failure cannot roll back committed incident evidence.
  • An incident alert reaches the team's owners and admins, its responders first, and at most 100 people per event.
Full capability reference
Operator checksproduct.operationsv0.3.4

Checks and actions your product registers, each with its own authorization, timeout and concurrency limit, run under idempotent receipts that keep safe evidence.

In the product
Users run a check, or an action that changes something once they confirm it, and read how each run ended. Linklet’s Library Health, above, is one way to show it.
Across the API
GET /operations/runs lists the newest runs, at most 100, for a caller holding operations:read. Linklet lists and runs its own targets through its /security/operations routes.
On the server
Runs only targets the product registered, and an action needs its exact target ID as confirmation. One tenant and idempotency key make one receipt, and a finished operation never runs twice.

What it guarantees

  • Request data never reaches commands or credentials
  • Receipts keep the target’s version and digest
  • A failed run shows product copy, never the cause

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. It is not a deployment tool, a shell runner or a replacement for the host’s observability.

Included dependencies (2)
  • Agent surfaceapi.agent-surface
  • Accounts and email sign-inidentity.human
Declared rules (11)
  • Only product-registered targets execute.
  • Every target supplies product-owned authorization, a bounded timeout, and a bounded concurrency limit.
  • Timed-out handlers retain their concurrency slot until they exit, bounding ignored cancellation.
  • Actions require the exact target identifier as confirmation; checks never masquerade as actions.
  • Tenant and idempotency key identify one receipt and a terminal operation never executes twice.
  • An operation that declares a subject kind runs for exactly one named subject; a replay naming another subject, target, actor or delegation fails closed.
  • Receipts snapshot target version and digest and move once from running to a terminal state.
  • Delegated executions preserve the original actor and an immutable delegation reference without creating another identity.
  • Durable evidence contains bounded safe codes and summaries; trusted logs retain diagnostic errors under the receipt identifier.
  • A failed run's summary is product copy for the person who ran it; the failure code and receipt reach operators in a separate detail field, never the cause.
  • No operation accepts commands, executable source, credentials, or arbitrary environment access through request data.
Full capability reference
Product analyticsproduct.analyticsv0.4.3

A versioned tracking plan, append-only consent receipts and a local event ledger. Events carry only classified values, and every one expires.

In the product
Users turn usage sharing on or off and see how many events are recorded. Linklet adds Delete Recorded Usage, which clears them all at once.
Across the API
GET /analytics reads the snapshot, POST /analytics/consents records a choice and POST /analytics/events records an event. DELETE /analytics/events/expired removes the owner’s expired events.
On the server
Refuses an event for a non-essential purpose without a current, granted consent receipt, and accepts only classified enum, identifier, integer and boolean values. An hourly purge removes expired events.

What it guarantees

  • Only the person’s own session records consent
  • Freeform text, URLs and emails are refused
  • Each event keeps its tracking plan’s digest

Supported starting points

  • SwiftUI + Go + SQLite
  • SwiftUI + Go + PostgreSQL
  • Go API + PostgreSQL
  • Go API + SQLite
  • Go + HTMX + PostgreSQL
  • Go + HTMX + SQLite
  • React + Go + PostgreSQL

Known limit. Cookieless collection, sessions, funnels and rollups are out of scope. Events stay in your database; sending them to a provider is a host job that reads the ledger.

Included dependencies (3)
  • Agent surfaceapi.agent-surface
  • Data classificationdata.classification
  • Accounts and email sign-inidentity.human
Declared rules (9)
  • Product source registers immutable versioned event definitions before ingestion.
  • Non-essential purposes require a current explicit granted consent receipt for the authenticated subject.
  • Properties are limited to declared classified enum, identifier, integer, and boolean fields; freeform text, URLs, email addresses, and credentials are rejected by design.
  • Event IDs are idempotent per tenant and every record retains the exact tracking-plan digest.
  • Every event has an explicit expiry. The owner-scoped purge removes only that owner's expired events; the cross-account purge removes only expired events, at most its limit a call, and runs only from host composition, never from a request.
  • The capability purges expired events across every account each hour unless a product hands the purge to something else, such as an enforced retention policy, with HandOffPurge.
  • Provider delivery reads the durable local ledger through host jobs; request handlers do not perform fragile synchronous dual writes.
  • The server does not start while an event property names a class the classification vocabulary does not define.
  • A retention policy that the cross-account purge enforces keeps exactly the days every registered event keeps; CheckRetention refuses any other.
Full capability reference
Explore Operations and insight

The complete public catalog, captured . Explore the live catalog

Your agent integrates.
You make the call.

The CLI, local MCP server and Agent Skills give your coding agent the same source and integration guidance. Every install starts with a plan you review.

  1. 1

    Find the right feature

    Your agent inspects the capabilities, dependencies and fit for your project.

    Read the agent brief
  2. 2

    Review the plan

    Inspect the proposed changes before anything is written.

    ./corbelo install --json
  3. 3

    Approve the source

    Approval is tied to the exact plan by its digest. Your project records what was installed.

    .corbelo/lock.json

Evidence travels with the feature. Published check records describe the commands, results and limits of those checks. Your own app still needs its own verification.

Inspect a check record

One contract.
Two interfaces.

Sign-in, shown in the same demo product on Apple and the web. The supplied UI is functional source you can use, restyle or replace while keeping the feature logic.

SwiftUINative Apple client
Sign in to the Linklet demo app on iPhone, using Corbelo's native SwiftUI interfaceSign in to the Linklet demo app on iPhone, using Corbelo's native SwiftUI interface
The Apple sign-in interface.
HTML with HTMXThe same sign-in feature in a browser
The same Linklet sign-in feature in a browser, sharing the API contract with the Apple clientThe same Linklet sign-in feature in a browser, sharing the API contract with the Apple client
Functional source, ready to make your own.
Explore Sign-in and its capabilities

From the first feature
to the whole product.

I designed and built Corbelo end to end: the feature catalog, the source and contracts behind it, the tools that agents use, corbelo.dev and its service, and the native Mac companion.

Why I’m building Corbelo
Product and platform
The catalog, starting points and maintained features.
Agent tooling
The CLI, local MCP server, Agent Skills and public catalog tools.
Web and native
The website, product service and Corbelo for Mac.

Explore the whole product.

The features, the stack and the thinking behind Corbelo.

Public prelaunch. Reservations take no payment.

Feature details