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.

- 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
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.
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-in
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 email
communication.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.
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-in
identity.human - Sessions and security history
identity.security-center - Product settings
product.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.
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 linking
identity.external-login - Provider readiness
provider.readiness - Secret references
security.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.
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 linking
identity.external-login - Provider readiness
provider.readiness - Secret references
security.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.
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 linking
identity.external-login - Provider readiness
provider.readiness - Secret references
security.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.
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 linking
identity.external-login - Provider readiness
provider.readiness - Secret references
security.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.
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 limits
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 history
identity.security-center - Agent surface
api.agent-surface - Accounts and email sign-in
identity.human - Secret references
security.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.
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-in
identity.human - Product settings
product.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.
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 decisions
security.risk - Durable rate limits
security.rate-limit - Agent surface
api.agent-surface - Accounts and email sign-in
identity.human - Secret references
security.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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-in
identity.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.
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 SCIM
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 trail
audit.trail - Teams and roles
collaboration.teams - Provider sign-in and linking
identity.external-login - OpenID Connect sign-in
identity.external-login.oidc - Custom domains
network.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.
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 trail
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 roles
collaboration.teams - Audit trail
audit.trail - Agent surface
api.agent-surface - Transactional email
communication.email - Accounts and email sign-in
identity.human - Product settings
product.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.
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 roles
collaboration.teams - Team invitations
collaboration.invitations - Agent surface
api.agent-surface - Feature access by plan
commerce.entitlements - License keys
commerce.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.
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 roles
collaboration.teams - Access policies
security.policy - Accounts and email sign-in
identity.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 surface
api.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.
Sync and realtime
Data & collaborationOffline records on Apple devices that sync with explicit conflicts, and live updates between people working together.2 capabilitiesOffline syncLive updates
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 surface
api.agent-surface - Public pages
content.public-pages - Accounts and email sign-in
identity.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.
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-in
identity.human - Domain events
integration.events - Provider readiness
provider.readiness - Permissions and custom roles
security.authorization - Secret references
security.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.
Feedback and roadmap
Data & collaborationPrivate product requests and an independently curated public roadmap, with operator review, safe links and deliberate publication.1 capabilityFeedback and roadmap
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 surface
api.agent-surface - Audit trail
audit.trail - Accounts and email sign-in
identity.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.
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 limits
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 surface
api.agent-surface - Accounts and email sign-in
identity.human - Provider readiness
provider.readiness - Secret references
security.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.
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 checkout
commerce.billing - Agent surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 plan
commerce.entitlements - Agent surface
api.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.
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 keys
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 surface
api.agent-surface - Plans and checkout
commerce.billing - Feature access by plan
commerce.entitlements - Accounts and email sign-in
identity.human - Provider readiness
provider.readiness - Secret references
security.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).
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 surface
api.agent-surface - Plans and checkout
commerce.billing - Accounts and email sign-in
identity.human - Provider readiness
provider.readiness - Durable rate limits
security.rate-limit - Secret references
security.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.
Search and commands
DiscoveryFull-text search across the records a person can see, and a command palette over everything they can do.4 capabilitiesSearch indexFull-text searchCommandsCommand palette
Full-text search across the records a person can see, and a command palette over everything they can do.
- API routes
- 10
- data tables
- 4
- migrations
- 5
- declared rules
- 30
Feature source before its dependencies. Stack support is listed for each capability.
Search indexsearch.indexingv0.4.5
Your records copied into one search index, kept current as they change and rebuilt from the source whenever needed. Each product decides which of its records are searchable.
- In the product
- A link a user just made or edited is findable at once. Rebuilding the index never loses a record users can see.
- Across the API
- GET /search/sources lists the kinds of record the index holds. POST /search/reindex rebuilds one kind of record for the caller.
- On the server
- Can write each record’s search entry in the same transaction as the record, so a rolled-back change leaves nothing behind. A rebuild replaces one owner’s entries for one kind at once, bounded to ten thousand.
What it guarantees
- Each owner’s entries stay theirs
- Result links stay inside your app
- The index can always be rebuilt
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 surface
api.agent-surface - Accounts and email sign-in
identity.human - Background jobs
workflow.jobs
Declared rules (8)
- Product bridges translate owned records into neutral search documents without creating an inbound Forge dependency.
- Tenant identity is supplied separately and is the first argument to every scoped store method.
- Incremental index writes can participate in the source record transaction.
- Full source rebuilds replace one tenant and source atomically and are bounded to ten thousand documents per job.
- Search result URLs are application-relative paths, never externally supplied redirect targets.
- Index documents are projections and may be deleted and rebuilt from authoritative product data.
- Rebuild jobs are at-least-once and source snapshot functions must be deterministic and side-effect free.
- A shared document belongs to a product-defined tenant and carries exactly one scope; the product decides which scopes a caller holds and the index never widens them.
Full-text searchsearch.queryv0.4.3
Full-text search over the index, ranked so a title match comes first. Users type plain words, and nothing they type is treated as search syntax.
- In the product
- Users search everything they own from one box and see the best matches first, titles before descriptions and content.
- Across the API
- GET /search answers up to fifty records for the caller, filtered by kind when asked.
- On the server
- Every query names the caller’s own index and repeats that limit in every join. Queries are bounded to twelve words, two hundred characters and fifty results.
What it guarantees
- Typed text is never search syntax
- No query reaches another owner’s records
- Ties rank the same way every time
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)
- Search index
search.indexing - Agent surface
api.agent-surface
Declared rules (7)
- User query text is tokenized as plain words and never interpreted as raw FTS syntax.
- Every FTS expression includes an indexed tenant qualifier and every relational join repeats the tenant predicate.
- Queries are bounded to twelve terms, two hundred input characters, and fifty returned records.
- Title matches rank above summaries, keywords, and full content with deterministic recency and identity tie breakers.
- The FTS table is a rebuildable acceleration structure maintained transactionally from canonical search documents.
- Rendered result text remains escaped and result URLs were validated as application-relative paths during indexing.
- Shared search runs only inside the scopes the product resolved for the caller from current authority; no scopes means no results, and a client never supplies the scopes.
Commandscommands.registryv0.4.4
One list of the things users can do in your product, each one checked against the person asking. Every surface lists and runs commands the same way.
- In the product
- Users see only the commands they may run. A command either opens a page of your app or runs an action you wrote.
- Across the API
- GET /commands lists the commands the caller may run. POST /commands/{commandId} runs one, and a retry with the same idempotency key replays the first answer.
- On the server
- Your product authorizes every command for the current caller before it is listed or run. Actions run inside the operation’s transaction, and their errors never reach the person.
What it guarantees
- Only allowed commands are listed
- Automations never list or run commands
- A retried command runs 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 (2)
- Agent surface
api.agent-surface - Accounts and email sign-in
identity.human
Declared rules (8)
- Every visible or executable command passes product-owned authorization for the current actor.
- commands.list and commands.run admit owner.session, owner.key and owner.agent and never automation; a command that names its own Actors names only kinds among those, Register refuses any other, and every other kind of caller neither lists it nor runs it, refused with actor_not_allowed through registry.ActorNotAllowed before its authorizer runs; commands.list answers an empty list to a kind the product's commands.run does not admit; on the owner's MCP server neither operation offers a command only the person's session may run, to any caller.
- Navigation commands go to a path on this origin, which may carry a query and a fragment, to an app route name, or to both, and never run handlers.
- Every surface lists and runs commands through the commands.list and commands.run operations, so an action's handler holds the operation's transaction, sees its actor's kind, and a retried run with the same Idempotency-Key replays rather than runs again.
- Action commands declare effects and execute only through bounded panic-contained callbacks.
- Raw authorization and handler errors never reach command consumers.
- Registry snapshots are deterministic and do not expose mutable callback-bearing definitions.
- A command's scope is caller-supplied context, never authority; an authorizer that reads it checks the actor's access to it.
Command palettecommands.palettev0.1.6
A command palette that searches commands and the person’s own records together, forgiving typos. It works in the browser and in SwiftUI.
- In the product
- Users open the palette, type a few letters or a misspelling, and pick a record or a command. If record search fails, commands still work.
- Across the API
- GET /palette answers matching commands and records for the caller. Browsers load the shared matching from /assets/commands-palette.js.
- On the server
- The signed-in person is the only source of whose records are searched, and typo corrections draw only on that person’s records. Queries are bounded to eighty bytes and fifty results a group.
What it guarantees
- Exact matches come before corrections
- Corrections never reveal another person’s words
- Actions still run through commands
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)
- Full-text search
search.query - Commands
commands.registry
Declared rules (7)
- The authenticated actor is the only source of owner identity. Vocabulary and records repeat owner and exact source/kind filters.
- Command discovery preserves commands.list and commands.run effective actor kinds, product authorization and session-only MCP filtering; actions always execute through commands.run.
- Literal full-text results precede fuzzy corrections. Correction dictionaries contain only the current owner's filtered records.
- Queries are at most 80 bytes. Each group returns at most fifty items. Vocabulary reads page at 256 terms and fail explicitly on deadline, without silently truncating candidates.
- Case and accents fold for local command matching; subsequences and bounded adjacent-transposition edits use deterministic ranking.
- Search failure keeps authorized commands usable and reports partial or unavailable record search without exposing provider errors.
- Palette clients debounce, cancel previous requests, discard stale answers, restore focus and respect text-entry and Reduce Motion.
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 routing
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 readiness
provider.readiness - Secret references
security.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.
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 email
communication.email - Notification choices
communication.preferences - Background jobs
workflow.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 settings
product.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.
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 email
communication.email - Notification choices
communication.preferences - Message templates
communication.templates - Accounts and email sign-in
identity.human - Product settings
product.settings - Background jobs
workflow.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 email
communication.email - Agent surface
api.agent-surface - Accounts and email sign-in
identity.human - Background jobs
workflow.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.
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 access
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 surface
api.agent-surface - Audit trail
audit.trail - Notification routing
communication.routing - Files
content.assets - Accounts and email sign-in
identity.human - Domain events
integration.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.
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 cases
communication.cases - AI tools and budgets
ai.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.
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 surface
api.agent-surface - Audit trail
audit.trail - Data classification
data.classification - Retention and legal holds
data.retention - Accounts and email sign-in
identity.human - Operator checks
product.operations - Permissions and custom roles
security.authorization - Approvals
workflow.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.
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 documents
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 readiness
provider.readiness - Secret references
security.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.
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 storage
storage.object - Agent surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 storage
storage.object - Files
content.assets - Agent surface
api.agent-surface - Audit trail
audit.trail - Accounts and email sign-in
identity.human - Domain events
integration.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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-in
identity.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.
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 surface
api.agent-surface - Audit trail
audit.trail - Data classification
data.classification - Retention and legal holds
data.retention - Accounts and email sign-in
identity.human - Permissions and custom roles
security.authorization - Explainable risk decisions
security.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.
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 storage
storage.object - Agent surface
api.agent-surface - Accounts and email sign-in
identity.human - Background jobs
workflow.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.
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 holds
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 classification
data.classification - Agent surface
api.agent-surface - Accounts and email sign-in
identity.human - File storage
storage.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.
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 classification
data.classification - Agent surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 assistants
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 surface
api.agent-surface - API keys
identity.api-key - Usage and limits
commerce.metering - Observability
operations.observability - Secret references
security.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.
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-in
identity.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.
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-in
identity.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 UI
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
Integrations
Developer platformSigned webhooks in and out, versioned events with replay, and a Slack connection with encrypted credentials.5 capabilitiesDomain eventsInbound webhooksOutbound webhooksSlack connectorConnected credentials
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 surface
api.agent-surface - Audit trail
audit.trail - Background jobs
workflow.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.human - Secret references
security.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.
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 surface
api.agent-surface - Feature access by plan
commerce.entitlements - Usage and limits
commerce.metering - Accounts and email sign-in
identity.human - Provider readiness
provider.readiness - Secret references
security.secret-references - Background jobs
workflow.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.
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 events
integration.events - Connected credentials
security.connected-credentials - Passkeys and two-step sign-in
identity.assurance - Permissions and custom roles
security.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.
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 surface
api.agent-surface - Audit trail
audit.trail - Accounts and email sign-in
identity.human - Secret references
security.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.
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 readiness
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.
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-in
identity.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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.
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.
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.
Jobs and workflows
Automation & AIBackground jobs, schedules in the owner's time zone, approvals of exact actions, and automations owners build themselves.4 capabilitiesBackground jobsSchedulesApprovalsAutomations
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 jobs
workflow.jobs - Agent surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 jobs
workflow.jobs - Agent surface
api.agent-surface - Audit trail
audit.trail - Notification routing
communication.routing - Accounts and email sign-in
identity.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.
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 jobs
workflow.jobs - Schedules
workflow.scheduling - Approvals
workflow.approvals - Agent surface
api.agent-surface - Audit trail
audit.trail - Feature access by plan
commerce.entitlements - Usage and limits
commerce.metering - Accounts and email sign-in
identity.human - Domain events
integration.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.
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 evaluations
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 readiness
provider.readiness - Secret references
security.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.
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 providers
ai.providers - Agent surface
api.agent-surface - Audit trail
audit.trail - Usage and limits
commerce.metering - Approvals
workflow.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.
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 providers
ai.providers - Agent surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 analytics
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 surface
api.agent-surface - Teams and roles
collaboration.teams - In-app notices
communication.messaging - Notification choices
communication.preferences - Observability
operations.observability - Provider readiness
provider.readiness - Secret references
security.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.
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 surface
api.agent-surface - Accounts and email sign-in
identity.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.
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 surface
api.agent-surface - Data classification
data.classification - Accounts and email sign-in
identity.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.
No features match that search.
Try a capability such as passkeys, offline sync, or webhooks, or reset the filters.
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
Find the right feature
Your agent inspects the capabilities, dependencies and fit for your project.
Read the agent brief - 2
Review the plan
Inspect the proposed changes before anything is written.
./corbelo install --json - 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 recordOne 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.




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.