app

package
v0.59.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 83 Imported by: 0

README

app

import "github.com/kitsunium/sdk/framework/internal/app"

Package app — who may call an operation: the principal, the rules and their checks.

Package app — activities: what a daemon's idle stop waits for besides its connections, a notion of the product's own — sessions still open, work still queued.

Package app — the App: the services a product mounts, served by one process.

Package app — the static analysis a product can be given: the platform's analyzer, run in the background in dev (ADR 0147 §1).

Package app — process profiles: what an app does with its process (ADR 0147 §5) — serve, run as a daemon, or run one CLI command — and the singleton lock.

Package app — the architecture the graph draws: where each store keeps its data.

Package app — authentication: the handler that says who calls, and the user it names.

Package app — the build a graph describes: the modules and versions it was made of.

Package app — the catalog of the generic mechanics a node is built from.

Package app — the app tag: the classes and words a field of personal data carries.

Package app — the plan of a Go type's classified members, computed once per type.

Package app — the walk of a value along its classification plan.

Package app — the warnings for fields whose name suggests personal data left unclassified.

Package app — CLI commands: a short command of the product that Main runs once, in the CLI profile, and whose status is the process's.

Package app — commands: an operation that changes something, with one handler.

Package app — the app's options and environments.

Package app — the config command: every setting, its value and where it comes from.

Package app — the consumer that drains a queue into its handler.

Package app — the daemon: what a running app knows of itself beyond its nodes.

Package app — the daemon's HTTP server as the Studio sees it: its loop, its counters, and the middleware every request goes through.

Package app — the daemon's loops: their states, the scheduler that fires the jobs, and the goroutine labels that name them.

Package app — databases: the SQL engines a store can be kept on.

Package app — the start's judgement of the declared databases.

Package app — the interfaces of databases: the engine a connector gives, and what the framework asks of it.

Package app — the migrations a database runs, and their lock.

Package app — the placement of each store: which database keeps its data.

Package app — a database in a running app.

Package app — the databases as the graph shows them.

Package app — the decoding of a request into its typed value.

Package app — the dev tools' routes of the Studio, read-only.

Package app is the implementation of framework/app, the framework in which every product is its own diagram. A product imports framework/app, whose names alias and forward to this package's: here, the types carry the role their name says — StoreService, EndpointService, AppConfigurer — and the facade keeps the names a product writes — Store, Endpoint, AppOption.

Package app — endpoints: an HTTP route and the operation it serves.

Package app — the errors a product returns to its callers, and their wire mapping.

Package app — exposures: an operation an endpoint serves over HTTP.

Package app — what an exposure takes, and what it exposes.

Package app — the Go modules of the running binary, read from its build info.

Package app — the graph: the product as a diagram of what it mounts.

Package app — history: the former values of the fields that keep them.

Package app — the history's storage: former values kept beside each record.

Package app — reading a record's former values.

Package app — the history as the graph and the Studio show it.

Package app — what the framework's own writes mean to the history.

Package app — legal holds: records kept whatever their retention says.

Package app — secondary indexes of a store.

Package app — the Studio's read API: the graph, the events, the sources.

Package app — what the source endpoint needs of the root it reads.

Package app — how the source endpoint opens a file on Unix.

Package app — how the source endpoint opens a file on Windows: the link itself, never its target.

Package app — jobs: scheduled work run by the daemon's loop.

Package app — the privacy journal: what the framework did to personal data, and why.

Package app — idempotency keys and their leases.

Package app — the compile-time proof of which roles each declaration plays: the interfaces the app reaches its nodes and options through.

Package app — listeners: inbound ports that are not HTTP, a private socket speaking a versioned contract (ADR 0148).

Package app — the compile-time proof that a listener is a starter.

Package app — what a listener needs of the socket it accepts on.

Package app — the product's logger and the logs the Studio shows.

Package app — loops: work the daemon waits for, woken by time or by events.

Package app — mailers: a durable outbox and the transport that empties it.

Package app — what building an SMTP transport needs of its URL.

Package app — Main: the whole main function of a product.

Package app — the migrate command.

Package app — modules: a set of services a product mounts as one.

Package app — the start's checks of the modules an app mounts.

Package app — what a module keeps: its stores and its migrations.

Package app — the mount of a module: its prefix and its bindings.

Package app — nodes: what every declaration shares, and its source position.

Package app — the interfaces of nodes: what every declaration is, and the roles some play.

Package app — observation: the spans and events of a running product.

Package app — operations: a typed request answered by a typed response.

Package app — the password policy of a store's fields.

Package app — hashing and verifying passwords under their policy.

Package app — the warnings for endpoints that set passwords outside a policy.

Package app — ports: an operation a service needs and does not implement.

Package app — privacy: the framework's own service for holds and the journal.

Package app — erasure: a record's personal data removed as its retention would.

Package app — export: a person's data as the framework gives it back.

Package app — a store's personal data as the graph shows it.

Package app — the keys that index personal data without revealing it.

Package app — the Studio's privacy pages: the journal, the holds, the register.

Package app — the privacy command.

Package app — what the privacy command prints.

Package app — `privacy seal`: a whole store sealed now.

Package app — the process sample: what the running process uses.

Package app — the Studio's profiler: the live heap and the goroutines, folded onto the graph.

Package app — queries: an operation that reads and changes nothing.

Package app — queued commands: a command run later, from its queue.

Package app — rate limits on an endpoint, per client.

Package app — records: the product's data as modules and the Studio reach it.

Package app — redaction: what a value shows once its secrets are hidden.

Package app — the register of processing: each store's line.

Package app — replacements: a test's substitute for an operation.

Package app — the HTTP request and response an endpoint can reach.

Package app — retention: how long a store keeps its personal data.

Package app — the retention of a store in a running app.

Package app — one pass of retention: what is due is erased or deleted.

Package app — record revisions: a store's previous versions of each record.

Package app — what changed between two versions, and a version written back.

Package app — where a store's versions are kept, engine by engine.

Package app — what the privacy rules ask of a record's versions.

Package app — a record's version written back.

Package app — what the Studio sees of a record's versions.

Package app — the revisions command: a version of a record written back.

Package app — binaries and their process roles: one executable, several Apps, and the versioned contracts between them (D22).

Package app — schemas: the JSON Schema of an operation's types.

Package app — scopes: the parts of a singleton's lock or of a socket's path that are only known at the start — the user, the executable, a configuration directory —, so one declaration keeps one process per user, per installed copy or per configuration.

Package app — the current user outside Windows: the process's UID, read from the kernel. os/user would ask libc through cgo — linking the program dynamically — or read /etc/passwd, for a number the kernel gives at once.

Package app — the current user on Windows: its SID, from os/user, which reads the process token there without cgo.

Package app — sealing at rest: members sealed where the framework keeps them.

Package app — where the data keys are, against what they seal.

Package app — cryptographic erasure: a data key destroyed.

Package app — a document as it lies at rest, sealed member by member.

Package app — messages at rest: a queue's members sealed.

Package app — a store that seals: its records as they rest.

Package app — a store that seals, as the framework's port sees it.

Package app — secrets: the values the product declares and the operator provides.

Package app — the secrets command.

Package app — where the secrets are kept, per environment.

Package app — services: a bounded context and the building blocks it owns.

Package app — settings: a value the environment gives a service.

Package app — the interfaces of settings: the values a setting holds, and what the framework reads of a declaration.

Package app — frontends: static assets the product serves.

Package app — stores: a typed, keyed collection of entities.

Package app — the engines a store runs on: memory, files or a database.

Package app — stores on SQL: the SDK's document store over a database.

Package app — the framework's own migrations on a database.

Package app — telemetry: the numbers a product reports, exported on a private socket when the deployment asks for them (ADR 0149).

Package app — topics and subscriptions: asynchronous messages of one type.

Package app — transactions: app.Transact and its levels.

Package app — a transaction on a database, the database's own.

Package app — the data's writer turn, where there are no transactions.

Package app — a transaction writes one database: the dev warning.

Package app — watches: how a module hears the writes of a product's marked fields.

Package app — a watch in a running app: its queue and its consumer.

Package app — the sentences the framework says: English from its own table, the languages after it from framework/app/studio's catalogues.

Package app — workflows: a state machine over the entities of one store.

Package app — the ports the workflow engine reaches a store through.

Index

Constants

const CodeAppConfig errs.Code = 262672

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.16

const CodeAppListen errs.Code = 262674

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.18

const CodeAppRunning errs.Code = 262673

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.17

const CodeCLIFailed errs.Code = 262715

CodeCLIFailed marks the span of a CLI command that exited non-zero.

Code 0.4.2.59

const CodeCatalogue errs.Code = 262712

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.56

const CodeCommandEncode errs.Code = 262702

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.46

const CodeCommandKey errs.Code = 262700

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.44

const CodeCommandQueue errs.Code = 262701

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.45

const CodeCommandReentrant errs.Code = 262699

Commands and queries.

Code 0.4.2.43

const CodeDataCopy errs.Code = 262735

CodeDataCopy refuses a data copy (ADR 0178 C7): a database the product does not declare or cannot open, data kept in memory, a product that answers on its address, a table of the database that already holds a row; or reports a copy done whose file queues could not be moved aside.

Code 0.4.2.79

const CodeDataCopyDiverged errs.Code = 262736

CodeDataCopyDiverged reports a data copy whose fingerprints differ on both sides of a table, or whose records do not open through the framework once copied: the copy's transaction is rolled back, and the first key that differs is named.

Code 0.4.2.80

const CodeDatabaseConfig errs.Code = 262690

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.34

const CodeDatabaseMigrate errs.Code = 262688

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.32

const CodeDatabaseOpen errs.Code = 262687

Databases, ADR 0004.

Code 0.4.2.31

const CodeDatabaseUnavailable errs.Code = 262689

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.33

const CodeDatabaseVersion errs.Code = 262732

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.76

const CodeGaveUp errs.Code = 262745

CodeGaveUp marks a declared loop the framework stopped running: its runs failed more often than its GiveUpAfter allows, and the product is no longer live until the process restarts. A consumer and a hand-written loop give up through the SDK's supervisor, whose own code — lifecycle's SupervisorGaveUp — their liveness check answers.

Code 0.4.2.89

const CodeHandlerPanic errs.Code = 262666

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.10

const CodeHistoryLoad errs.Code = 262695

History (ADR 0007).

Code 0.4.2.39

const CodeHistoryMoved errs.Code = 262706

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.50

const CodeHistoryRead errs.Code = 262697

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.41

const CodeHistoryWrite errs.Code = 262696

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.40

const CodeJobOverlapped errs.Code = 262705

The framework's own signals and refusals, typed since the move into the SDK (rule 2: no fmt.Errorf, no errors.New in production code).

Code 0.4.2.49

const CodeKeyRekeyed errs.Code = 262710

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.54

const CodeLoopNotMounted errs.Code = 262681

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.25

const CodeLoopPanic errs.Code = 262680

Loops.

Code 0.4.2.24

const CodeLoopStart errs.Code = 262682

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.26

const CodeMailConfig errs.Code = 262675

Mailers.

Code 0.4.2.19

const CodeMailEncode errs.Code = 262676

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.20

const CodeMailPanic errs.Code = 262679

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.23

const CodeMailQueue errs.Code = 262677

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.21

const CodeMailUndecodable errs.Code = 262678

Deprecated: the framework no longer returns it. The outbox is the SDK's mail spool, which dead-letters a record that does not decode itself (MessageUndecodable, in pkg/v1/app/mail/spool).

Code 0.4.2.22

const CodeMigrateRefused errs.Code = 262711

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.55

const CodeMigrationIrreversible errs.Code = 262740

CodeMigrationIrreversible refuses the Down of a migration declared irreversible with Irreversible (ADR 0179 step 10): what it changed is undone by a new forward migration, never by migrate down.

Code 0.4.2.84

const CodeNotMounted errs.Code = 262667

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.11

const CodeNotSource errs.Code = 262707

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.51

const CodeNothingToErase errs.Code = 262709

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.53

const CodeOTLPExport errs.Code = 262746

CodeOTLPExport refuses a start whose OTLP export (OTLP) cannot be opened — its queue, its collector's endpoint, its headers —, says a stop that did not let it flush within its budget, marks a batch the product's own sender refused for good, and refuses the otlp command a queue it cannot read or a dead letter it does not hold.

Code 0.4.2.90

const CodePasswordHash errs.Code = 262698

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.42

const CodePasswordMoved errs.Code = 262708

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.52

const CodePrivacyData errs.Code = 262694

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.38

const CodePrivacyErase errs.Code = 262692

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.36

const CodePrivacyJournal errs.Code = 262693

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.37

const CodePrivacyKey errs.Code = 262691

Privacy (ADR 0006).

Code 0.4.2.35

const CodeProfileRead errs.Code = 262704

CodeProfileRead reports a profile of the process that could not be written or read back. It is in the framework's 0.4.2.* range.

Code 0.4.2.48

const CodeQueueOpen errs.Code = 262664

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.8

const CodeRecordsKeys errs.Code = 262733

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.77

const CodeRenameFailed errs.Code = 262743

CodeRenameFailed refuses a start whose move of a name at rest to its present name did not complete (ADR 0180 step 5): a directory the system would not rename, a statement the database did not run. What was not moved stays under its former name, readable as it was, and the next start resumes the move.

Code 0.4.2.87

const CodeRenameTarget errs.Code = 262742

CodeRenameTarget refuses a start that meets what a former release kept under the tool's name — a table, the privacy service's directory, the dev data directory — while its present name is taken too (ADR 0180 step 5): the framework never replaces one with the other, so nothing is moved and both are named. What rests under the former name stays as it was, readable by the release that wrote it.

Code 0.4.2.86

const CodeRenamed errs.Code = 262741

CodeRenamed refuses a start that meets a runtime name the framework no longer reads (ADR 0180 §3, Q12): an operator's variable still named KIT_<NAME> where the framework reads <PRODUCT>_<NAME>, a struct field still tagged kit:"…" where it reads app:"…". Ignored, the first would start the product on a default — an empty data directory —, the second would store a personal field in clear. The refusal is a DiagnosticsError naming every old name met and the name that replaces it, all at once; it carries this code, which errs.HasCode reads through it. The refusal holds for two minors, then the old names are forgotten.

Code 0.4.2.85

const CodeResealInPlace errs.Code = 262728

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.72

const CodeResealMoved errs.Code = 262729

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.73

const CodeRetentionPanic errs.Code = 262713

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.57

const CodeRetryIn errs.Code = 262744

CodeRetryIn marks a failure a handler returned through RetryIn: the queue of the subscription, the watch or the queued command that ran it hands the message back with the delay it asked for, the delivery counted. It joins the trail of an SDK cause, whose own code a dead letter records; any other cause is recorded under it.

Code 0.4.2.88

const CodeRevisionDecode errs.Code = 262725

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.69

const CodeRevisionRead errs.Code = 262723

Revisions (ADR 0007 §3).

Code 0.4.2.67

const CodeRevisionWrite errs.Code = 262724

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.68

const CodeScopeUnknown errs.Code = 262716

CodeScopeUnknown reports a scope whose value could not be read at the start: no current user, no executable path, no configuration directory.

Code 0.4.2.60

const CodeSealErased errs.Code = 262726

The signals the framework ends one of its own steps with and catches itself — a caller never receives one —, typed when the SDK made rule 2 a gate (make guard), where they had been stdlib errors.

Code 0.4.2.70

const CodeSealKey errs.Code = 262717

Sealing at rest (ADR 0006, step 3).

Code 0.4.2.61

const CodeSealKeyMoved errs.Code = 262727

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.71

const CodeSealOpen errs.Code = 262719

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.63

const CodeSealRewrap errs.Code = 262720

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.64

const CodeSealShred errs.Code = 262721

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.65

const CodeSealWrite errs.Code = 262718

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.62

const CodeSecretMissing errs.Code = 262684

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.28

const CodeSecretRead errs.Code = 262685

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.29

const CodeSecretRotate errs.Code = 262686

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.30

const CodeSecretStore errs.Code = 262683

Secrets.

Code 0.4.2.27

const CodeSingletonHeld errs.Code = 262714

CodeSingletonHeld reports a start refused because another process of the app holds its singleton lock.

Code 0.4.2.58

const CodeStoreDecode errs.Code = 262657

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.1

const CodeStoreEncode errs.Code = 262658

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.2

const CodeStoreIndex errs.Code = 262661

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.5

const CodeStoreLoad errs.Code = 262660

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.4

const CodeStorePersist errs.Code = 262659

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.3

const CodeSubsystemMissing errs.Code = 262739

CodeSubsystemMissing refuses a start whose declarations need a subsystem the program does not import (ADR 0179 §6): a store or a database without framework/app/store, a server without framework/app/server. The refusal is a DiagnosticsError naming every such declaration and the package to import, all at once; it carries this code, which errs.HasCode reads through it.

Code 0.4.2.83

const CodeTopicEncode errs.Code = 262662

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.6

const CodeTopicPublish errs.Code = 262663

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.7

const CodeTransactionNested errs.Code = 262747

CodeTransactionNested refuses a Transact inside another that asks for an isolation level or read-only (Serializable, RepeatableRead, ReadOnly): the inner level is a savepoint of the outer transaction, which can change neither.

Code 0.4.2.91

const CodeTransactionPanic errs.Code = 262731

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.75

const CodeTransactionReadOnly errs.Code = 262748

CodeTransactionReadOnly refuses a store's write inside a transaction opened with ReadOnly, on every backend, before it reaches the database.

Code 0.4.2.92

const CodeTransactionSpan errs.Code = 262722

A transaction wrote a store another database keeps than the one it belongs to — the data directory counting as one: refused, Invalid.

Code 0.4.2.66

const CodeTrash errs.Code = 262734

The trash (kitsunium/sdk ADR 0178 C4).

Code 0.4.2.78

const CodeTrashList errs.Code = 262738

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.82

const CodeUndecodable errs.Code = 262665

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.9

const CodeWatchQueue errs.Code = 262703

Watches (ADR 0008).

Code 0.4.2.47

const CodeWatchReplay errs.Code = 262737

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.81

const CodeWorkflowHookChange errs.Code = 262670

An OnEnter hook changed the state its entity was entering, or the entity's key: the transition is refused.

Code 0.4.2.14

const CodeWorkflowHookPanic errs.Code = 262669

A workflow hook panicked: an OnEnter hook fails its transition with it, an OnTransition hook is reported and the transition stands.

Code 0.4.2.13

const CodeWorkflowLoad errs.Code = 262668

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.12

const CodeWorkflowNotInPlace errs.Code = 262730

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

Code 0.4.2.74

const CodeWorkflowReentrant errs.Code = 262671

An OnEnter hook fired its own workflow: refused, where it would wait for itself.

Code 0.4.2.15

const DefaultMaxBody int64 = 1048576

DefaultMaxBody is the largest request body an endpoint reads, unless the endpoint says otherwise with MaxBody.

const DialectMySQL Dialect = "mysql"

DialectMySQL is MySQL or MariaDB.

const DialectPostgres Dialect = "postgres"

DialectPostgres is PostgreSQL.

const DialectSQLite Dialect = "sqlite"

DialectSQLite is SQLite.

const EnvDev = "dev"

EnvDev serves the Studio, analyzes the source and binds loopback. Only `kit dev` or an explicit <PRODUCT>_ENV=dev turns it on.

const EnvProduction = "production"

EnvProduction is the default: no Studio, no source, no analysis.

const Moderated Mark = "moderated"

Moderated marks content others see and a moderator may act on.

const OTLPBlock OTLPFullPolicy = "block"

OTLPBlock: the span or the snapshot waits for room a few seconds, then is refused, counted dropped.

const OTLPDropNewest OTLPFullPolicy = "drop-newest"

OTLPDropNewest: the new batch is refused, counted dropped.

const OTLPDropOldest OTLPFullPolicy = "drop-oldest"

OTLPDropOldest, the default: the oldest batches not being sent are taken out, counted dropped, until the new one fits.

const OTLPMetrics string = "metrics"

OTLPMetrics names the metrics signal: an OTLP/JSON ExportMetricsServiceRequest.

Initialised with github.com/kitsunium/sdk/framework/internal/app/plug.OTLPMetrics.

const OTLPTraces string = "traces"

OTLPTraces names the trace signal: an OTLP/JSON ExportTraceServiceRequest.

Initialised with github.com/kitsunium/sdk/framework/internal/app/plug.OTLPTraces.

const OverlapAllow OverlapPolicy = "allow"

OverlapAllow runs the fire beside the previous run.

const OverlapReplace OverlapPolicy = "replace"

OverlapReplace cancels the previous run's context and runs the fire once that run returned: the newest fire wins, two runs never overlap.

const OverlapSkip OverlapPolicy = "skip"

OverlapSkip skips the fire, counted, the previous run going on: the default, since the framework cannot know that two runs of a job may run at once.

const Personal Mark = "personal"

Personal marks a field that holds data about a person: personal, special, or a record's subject.

const Special Mark = "special"

Special marks a field of GDPR art. 9(1) or art. 10.

const WireCanceled = "canceled"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireConflict = "conflict"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireForbidden = "permission_denied"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireInternal = "internal"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireInvalid = "invalid_argument"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireNotFound = "not_found"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireRateLimited = "rate_limited"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireTimeout = "deadline_exceeded"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireTooLarge = "too_large"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireUnauth = "unauthenticated"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireUnavailable = "unavailable"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

const WireUnsupported = "unsupported_media_type"

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

Variables

var PerConfigDir ScopeValue

PerConfigDir is the user's configuration directory (os.UserConfigDir): one process per configuration home.

var PerExecutable ScopeValue

PerExecutable is the running executable's path, links resolved: one process per installed copy of the binary.

var PerUID ScopeValue

PerUID is the current user: one process per user of the machine (its UID on Unix, its SID on Windows).

var Privacy *Service

Privacy is the framework's own service: the data keys, the legal holds and the privacy journal of an app that keeps personal data or seals what it keeps. The framework mounts a copy of it — named app.privacy, a name no service of a product can take — in every app whose stores keep personal data or declare a retention, or that keeps sealed values at rest, before the product's services. Its stores stay in the data directory unless the product places them (ADR 0004): app.Keeps(app.Privacy). On a SQLite database its data keys stay in the data directory all the same — a key is written apart from every transaction, and the file's one writer may be held by the transaction of the write that needs it —; its holds and its journal go there. A product never mounts it itself.

func After

func After(ctx context.Context, d time.Duration) <-chan time.Time

After is time.After on the app's clock.

func AllowPeers

func AllowPeers(uids []int, gids []int) ListenerConfigurer

AllowPeers admits accounts besides the product's own: an on-call group declared at deployment. The kernel checks them where it names the peer; the socket's directory must still let them through, which is the deployment's to arrange.

IFACE-OPAQUE: ListenerOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to Listen.

func Analyze

func Analyze(on bool) AppConfigurer

Analyze switches the in-process static analysis on or off in dev.

func Analyzer

func Analyzer(fn AnalyzeFunc) AppConfigurer

Analyzer runs fn in the background once the product serves, in dev and when KIT_ANALYZE allows it. Without one, the graph is the runtime's alone.

func Anonymise

func Anonymise[T any](fn func(*T)) StoreConfigurer

Anonymise says what an erased record keeps: fn runs first, and generalises into unclassified fields — a birth date to its year, an address to its region; then the framework clears the rest. The framework cannot prove that what remains identifies no one: by declaring the function, the product asserts it.

func AnyUser

func AnyUser() ExposeConfigurer

AnyUser says that an exposure is open to any signed-in user, on purpose: its operation asks for a user (Auth) and declares no permission and no rule — its handler reads UserID and keeps to what that user owns.

var MyOrders = Service.Query("my-orders", myOrders, app.Auth()).Expose("GET /orders", app.AnyUser())

It adds no authentication of its own: an exposure authenticates as its operation asks, so the operation declares Auth.

func Anyone

func Anyone() ExposeConfigurer

Anyone says that an exposure is open to everyone, on purpose: its operation declares no permission and no rule, and whoever reaches the route may run it — a public form, say. It says so on the exposure's node, where the diagram and the API page read it. An exposure whose operation declares no rule refuses the start unless it says this, or AnyUser.

var Report = Service.Command("report", file).Expose("POST /reports", app.Anyone())

It adds no authentication: an operation that asks for a user (Auth) takes AnyUser instead, and one with AuthOptional still knows the caller who signed in.

func Auth

func Auth() OperationOption

Auth makes an endpoint require an authenticated caller: the app's auth handler runs first, and a request without valid credentials is answered 401 before the handler runs. A command or a query that asks for one is refused 401 when its caller carries no user: an in-process dispatch carries its caller's, and is not authenticated again.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func AuthData

func AuthData[D any](ctx context.Context) (D, bool)

AuthData returns what the auth handler said about the caller, and whether there is a caller whose data has type D.

func AuthOptional

func AuthOptional() OperationOption

AuthOptional runs the app's auth handler when the request carries credentials, and serves anonymous callers too: UserID tells which.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func AutoExtend

func AutoExtend(on bool) DeliveryOption

AutoExtend says whether the lease of the message a handler is running is renewed, a third of the lease after it was granted or last renewed, for as long as the handler runs — a subscription's, a watch's, or a queued command's. It is on by default: a slow handler keeps its message instead of seeing it delivered to another while it runs. Off, the handler is stopped a margin before its Lease ends. A renewal the queue refuses cancels the handler's context. A mailer's outbox never renews: the mail spool parks a mail by its own lease.

on is the default of the consumer's setting <service>-<kind>-<name>-auto-extend, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Bind

func Bind[Req, Resp any](port *PortService[Req, Resp], op Operation[Req, Resp]) interface{AppConfigurer; MountConfigurer}

Bind makes the app's port call op, whatever implements the port and whatever its fallback: an endpoint — or, with ADR 0005, a command or a query — of the port's types. A binding is code, the same in every environment; a binding to a port, or to a service the app does not mount, is refused when the app starts. Given twice for one port, the last wins.

a := app.NewApp("vigie", desk.Service, posts.Service).With(app.Bind(desk.Enforcer, posts.EnforceAPI))

It is an option of Mount too, under the same name: there it binds a port of the module mounted, and another is refused.

a := app.NewApp("shop", posts.Service).With(app.Mount(moderation.Module, app.Bind(moderation.Enforcer, posts.EnforceAPI)))

func Breaker

func Breaker(failures int, open time.Duration, opts ...BreakerConfigurer) PortPolicyConfigurer

Breaker stops a port's calls from reaching a dependency that keeps failing: after failures failures of the dependency's in a row — or, given BreakerWindow, a share of a window's calls — it opens, and every call fails at once with the SDK's CircuitOpen (503) for open; then it lets HalfOpenTrials trial calls through, one at a time, which close it or open it again, for longer each time under OpenBackoff. A refusal of the caller's own does not count, nor a caller that left, nor a throttle's refusal. It is the SDK's resilience circuit breaker, on the app's clock, one per port and per start.

Its state is the product's: a readiness check, breaker:<port>, degraded — never unready — while it is not closed; each change of phase logged and put on the telemetry port. An operator opens it until reset with the product's `breaker isolate PORT` and closes it with `breaker reset PORT`, kept in the data directory: a start applies it, and so does a running product, within five seconds.

The values are the defaults of the port's settings <service>-port-<name>-breaker-failures, -breaker-open, -breaker-open-max, -breaker-trials, -breaker-window, -breaker-window-calls and -breaker-window-ratio, which config/<env>.* and <PRODUCT>_<NAME> override; a value the breaker cannot run with stops the start, and so does a breaker the SDK refuses.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func BreakerWindow

func BreakerWindow(size time.Duration, minCalls int, failureRatio float64) BreakerConfigurer

BreakerWindow makes a Breaker open on a rate rather than on failures in a row: once the last size of calls holds at least minCalls, a share of failures of failureRatio or more opens it. Its values are the defaults of the port's settings <service>-port-<name>-breaker-window, -breaker-window-calls and -breaker-window-ratio; a window of 0 is none, a ratio outside (0, 1] stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Budget

func Budget(maxTokens int, tokenRatio float64) PortPolicyConfigurer

Budget sizes the retry budget a port's Retry and Hedge share: a bucket of maxTokens tokens, a failure costing one and a success putting back tokenRatio; retries and duplicates are refused once half the tokens are spent, so a dependency that fails for every caller receives fewer extra calls, not every call times the attempts. A port that retries or hedges without it has 10 tokens and a ratio of 0.1, gRPC's example; a Budget on a port that does neither stops the start. It is the SDK's resilience retry budget, one per port and per start.

The values are the defaults of the port's settings <service>-port-<name>-budget-tokens and -budget-ratio, which config/<env>.* and <PRODUCT>_<NAME> override; fewer than one token, or a ratio below a thousandth, stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Bulkhead

func Bulkhead(maxConcurrent int, opts ...BulkheadConfigurer) OperationOption

Bulkhead caps how many requests the handler serves at once; the overflow answers 503 — at once, or, given BulkheadQueue and MaxWait, once the queue is full or the wait ran out. It is the SDK's resilience bulkhead.

The values are the defaults of the operation's settings <service>-<kind>-<operation>-bulkhead, -bulkhead-queue and -bulkhead-wait, which config/<env>.* and <PRODUCT>_<NAME> override; a bulkhead below one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func BulkheadQueue

func BulkheadQueue(n int) BulkheadConfigurer

BulkheadQueue lets up to n calls beyond those a Bulkhead runs wait for a slot, each for its MaxWait: without a wait, or with n at 0, the overflow is refused at once, and a negative n stops the start. A call that finds the queue full answers 503. It is the default of the operation's setting <service>-<kind>-<operation>-bulkhead-queue, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Catalog

func Catalog() []model.Mechanic

Catalog lists the generic mechanics the framework offers: the building blocks a node composes rather than implements, each one backed by an SDK package. It is part of every graph, so the Studio — and an AI agent reading the graph — knows what can be added to a product and the exact code that adds it.

func ClearCookie

func ClearCookie(ctx context.Context, name string)

ClearCookie tells the browser to forget the cookie called name.

func ClientIP

func ClientIP(ctx context.Context) string

ClientIP returns the address of the client of the request ctx serves: the connection's peer, or — only when <PRODUCT>_TRUST_PROXY=on — the last address of X-Forwarded-For, the one the trusted proxy appended. It is "" outside an HTTP request.

func Clock

func Clock(c clock.Timed) AppConfigurer

Clock makes the app read and wait on c instead of the system clock: a test drives timer transitions and jobs with a clock.ManualClock.

func ConfigFiles

func ConfigFiles(fsys fs.FS) AppConfigurer

ConfigFiles gives the framework the product's configuration files: config/config.<ext>, which every environment reads, and config/<env>.<ext>, the environment's own — <env> being <PRODUCT>_ENV as written, dev when kit dev runs the product — in JSON, YAML or TOML (json, yaml or yml, toml). A file maps the names of the settings to their values; it never holds a secret, and a key no service declares stops the start. The files belong in the binary:

//go:embed config
var configFiles embed.FS

a := app.NewApp("todo", ...).With(app.ConfigFiles(configFiles))

func Conflict

func Conflict(message string) *Error

Conflict reports a request the resource's current state refuses (409).

func DataDir

func DataDir(dir string) AppConfigurer

DataDir sets where stores and queues persist, instead of <PRODUCT>_DATA_DIR. Given to an app after InMemory, it wins: the last of the two says where the data is — so a test helper that runs the product in memory runs it on disk with the one option its test gives (apptest.NewDisk).

func Database

func Database(name string, engine Engine, opts ...DatabaseConfigurer) AppConfigurer

Database declares a database the product keeps its stores in, named name, on engine — an engine module's, which the product's main imports:

import (
	"github.com/kitsunium/sdk/framework/connectors/postgres"
	"github.com/kitsunium/sdk/framework/connectors/sqlite"
)

var App = app.NewApp("vigie", intake.Service, desk.Service, audit.Service).With(
	// Keeps every store no other database keeps.
	app.Database("database", postgres.Engine()),
	// The audit trail apart.
	app.Database("archive", sqlite.Engine(), app.Keeps(audit.Service)),
)

name is 1 to 49 lower-case letters, digits and dashes, not starting with "app-" nor "kit-", unique among the app's databases. It names the database's URL, the secret <name>-url — the variable <APP>_<NAME>_URL, or <APP>_<NAME>_URL_FILE naming a file that holds it, then the environment's secret store —, and its settings: <name>-max-open (10), <name>-max-idle (2), <name>-max-lifetime (30m), <name>-max-idle-time (0: none), <name>-timeout (5s), <name>-check-timeout (5s), <name>-tx-retries (3: how many times a transaction the database asks to run again runs again, 0 for never — Classifier), <name>-tx-retry-delay (50ms: the first wait before it does) and <name>-migrate (start, or manual). Outside dev the URL is required and writes its TLS mode; in dev a database without one leaves its stores in the data directory, and a SQLite database without one is the file <data>/<name>.sqlite. InMemory on the app opens none.

func DefaultCommand

func DefaultCommand() CommandLineConfigurer

DefaultCommand makes the command Main's default: it runs with every argument when the first names no other command — neither one of the product's nor one of Main's (serve, help…) —, and with none when there is none. Main then never answers a usage error. One command of an app may be its default.

IFACE-OPAQUE: CLIOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to CLI.

func DeleteAfter

func DeleteAfter[T any, D Delay](delay D, since func(T) (time.Time, bool)) StoreConfigurer

DeleteAfter deletes a record delay after the instant since returns.

func DeleteAt

func DeleteAt[T any](at func(T) (time.Time, bool)) StoreConfigurer

DeleteAt deletes a record at the instant at returns.

func DeleteOnErasure

func DeleteOnErasure() StoreConfigurer

DeleteOnErasure makes a person's erasure (app.Erase) delete the store's records instead of clearing them, as sessions and accounts want.

func DesignDigest

func DesignDigest(digest string) AppConfigurer

DesignDigest records the digest of the design the product's code was generated from — what the generated wiring passes —, so the telemetry handshake can say it and a tool attached to the product can tell a process built from another design.

func Drain

func Drain(d time.Duration) DeliveryOption

Drain is how long the message a handler is running when the app stops may still run before its context is cancelled — a subscription's, a watch's, or a queued command's. A handler that finishes within it is acknowledged; one it cuts short is handed back uncounted, delivered again at the next start, and is no failure. The default is 5 seconds; zero cancels the handler at once, and a negative one stops the start. The app's stop gives each component the longest drain plus its settle (Settle), and tells every consumer at once, so that their drains overlap.

d is the default of the consumer's setting <service>-<kind>-<name>-drain, the kind subscription or command, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func EnableOTLP

func EnableOTLP()

EnableOTLP makes the framework's own export code reachable: the settings, the component, the readiness and the stop of the OTLP export a product declares. framework/app/otlp calls it as it is imported; a product never does.

func EnableServer

func EnableServer()

EnableServer makes an app in the server profile serve HTTP. framework/app/server calls it as it is imported; a product never does.

func EnableStores

func EnableStores()

EnableStores makes the framework's own store code reachable: what the framework mounts, starts and answers for the stores and databases a product declares. framework/app/store calls it as it is imported; a product never does.

func EnableStudio

func EnableStudio()

EnableStudio makes a serving app in dev mount the Studio's routes, which framework/app/studio plugs in (plug.Studio), over a host of the app. framework/app/studio calls it as it is imported; a product never does.

func Env

func Env(env string) AppConfigurer

Env selects the environment, instead of <PRODUCT>_ENV.

func Erase

func Erase(ctx context.Context, reason string, ids ...string) (Erasure, error)

Erase erases every record whose subject is one of ids, in every store of the app, as its retention would — or deletes it where the store says app.DeleteOnErasure — and folds the stores it wrote, so that what it overwrote leaves their files. A held record is left in place, and the result says so: it lists, per store, what was erased, deleted and held. Telling the recipients (GDPR art. 19) is the product's job; the register lists them. reason is journaled.

What the framework seals at rest, it erases cryptographically too (ADR 0006 §4): a held record is moved under a data key of its own, then the data key of each identity, and every data key the erased records were sealed under, is destroyed — every copy of what they sealed stops opening: a store's former values, a dead letter, a backup. An erasure that failed in a store destroys nothing: its retry does.

func EraseAfter

func EraseAfter[T any, D Delay](delay D, since func(T) (time.Time, bool)) StoreConfigurer

EraseAfter erases a record's personal data delay after the instant since returns — its closing, say; false means "not yet": an open record is never due. delay is a duration, or a duration setting.

func EraseAt

func EraseAt[T any](at func(T) (time.Time, bool)) StoreConfigurer

EraseAt erases a record's personal data at the instant at returns: one the product computed and stored on the record — a retention in calendar months, one that depends on a tenant.

func Export

func Export(ctx context.Context, ids ...string) (PersonalData, error)

Export returns every record whose subject is one of ids, in every store of the app: each without its secret members, with the former values its fields keep (ADR 0007) — never a secret field's — and the versions its store keeps of it, without their secret members, and per store what GDPR art. 15(1) asks beside the data — the purpose, the retention and the recipients inside the product. A held record is exported like any other. The result is JSON, structured and machine-readable (art. 20); keeping the month of art. 12(3) is the product's job, and so is deciding who may call it.

A person known under several identities — a user's ID in one store, an e-mail address in another — is found by giving all of them. ctx must run inside the app: an endpoint's, a job's, a loop's.

func FailSafe

func FailSafe() CommandLineConfigurer

FailSafe makes the command's status always 0: a status line, a prompt hook, anything a shell runs on every keystroke must never fail the shell. A non-zero status, a start that fails and a panic are logged instead.

IFACE-OPAQUE: CLIOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to CLI.

func Fallback

func Fallback[Req, Resp any](op Operation[Req, Resp]) PortConfigurer[Req, Resp]

Fallback gives a port an implementation from its own side, which it calls when the app binds nothing else: an endpoint — or, with ADR 0005, a command or a query — of the port's types. A port given here is refused when the app starts.

var Enforcer = Service.Port[Measure, Applied]("enforcer", app.Fallback(QueueAPI))

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Forbidden

func Forbidden(message string) *Error

Forbidden reports a caller who may not do this (403).

func From

func From(name string, addr string) MailerConfigurer

From sets the sender of every message that names none.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Generated

func Generated(bytes int) SecretConfigurer

Generated makes the secret the framework's to make: versions of bytes random bytes — 32 for a key — the first made on the first start, a new one every 30 days, the last 3 kept, unless RotateEvery and KeepVersions say otherwise.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func GiveUpAfter

func GiveUpAfter(restarts int, within time.Duration) SupervisionConfigurer

GiveUpAfter bounds how often the framework restarts what fails — a subscription's, a watch's or a queued command's consumer whose queue failed, a hand-written loop (Service.Go) that returned, a declared loop (Service.Loop) whose run failed or panicked —: once it failed more than restarts times within within, it is not restarted again, and the product is no longer live — its liveness check "supervised:<node>" fails, the problem names it — so that whatever watches its liveness — an orchestrator's probe — restarts the process; a product nothing probes keeps running without it, and says so in its log, its problems and its telemetry. It is Erlang's restart intensity, the SDK's lifecycle supervisor's GiveUpAfter.

The default is 10 within 10 minutes: with the restarts' backoff — one second, doubling up to a minute — a component that fails at every start is given up about five minutes after its first failure, and one that fails once a minute or less often is restarted forever. A within of zero never gives up; a negative value stops the start.

restarts and within are the defaults of the settings <service>-<kind>-<name>-give-up-restarts and -give-up-within, the kind subscription, command or loop, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its methods are unexported — so a caller only hands it to the declaration it configures.

func HalfOpenTrials

func HalfOpenTrials(n int) BreakerConfigurer

HalfOpenTrials is how many trial calls in a row must succeed to close a Breaker once its open time is over — one by default. It is the default of the port's setting <service>-port-<name>-breaker-trials; fewer than one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Hedge

func Hedge(delay time.Duration, inFlight int) PortPolicyConfigurer

Hedge races a port's slow call: an attempt that has not answered after delay is joined by a duplicate, and the first to succeed answers — the tail of a dependency's latency cut, at the price of more calls. At most inFlight duplicates run at once across every call, and the port's retry budget (Budget) refuses them while the dependency fails for many: then the call goes on with the attempts it has. Only a port declared Idempotent may hedge — the start refuses another. It is the SDK's resilience hedge, on the app's clock.

The values are the defaults of the port's settings <service>-port-<name>-hedge-delay, -hedge-in-flight and -hedge-max — the duplicates one call may make, 1 —, which config/<env>.* and <PRODUCT>_<NAME> override; a delay that is not positive, or fewer than one, stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func HeldUntil

func HeldUntil[T any](until func(T) (time.Time, bool), reason string) StoreConfigurer

HeldUntil holds each record until the instant until returns: the law's own retention, such as an invoice kept ten years (GDPR art. 17(3)(b)). reason is the legal ground, for the register. A held record is exported and updated like any other, and neither erased nor deleted.

func HoldSubject

func HoldSubject(ctx context.Context, reason string, ids ...string) error

HoldSubject places a legal hold on every record whose subject is one of ids, in every store of the app: an authority's request about a person. It holds the records they have now; a record written later is not held. reason is kept as Store.Hold keeps it.

func Idempotent

func Idempotent() PortPolicyConfigurer

Idempotent declares that the port's operation may run more than once for one call, concurrently, with no harm done: what a Hedge needs, which is refused on a port that does not say so. The graph draws it on the port.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func IdleStop

func IdleStop(d time.Duration) AppConfigurer

IdleStop makes a daemon stop itself once, for d, no connection was open on any of its listeners and no activity of its services (Service.Activity) said it was busy: a daemon launched on demand by its clients ends when they stop coming. It is refused outside the daemon profile.

func InMemory

func InMemory() interface{StoreConfigurer; AppConfigurer}

InMemory keeps data in memory. On an app, it overrides the data directory for every store and queue — <PRODUCT>_DATA_DIR, and a DataDir given before it: what a test wants. On a store, it keeps that store in memory even when the app has a data directory.

func Index

func Index[T any](name string, keys func(T) []string) StoreConfigurer

Index declares an index where an entity may have several keys and a key several entities — the users a task is shared with. StoreService.Find reads it. Empty keys are not indexed.

func Invalid

func Invalid(message string) *Error

Invalid reports a request the caller must change before retrying (400).

func Irreversible

func Irreversible(ctx context.Context, ex MigrationExecutor) error

Irreversible is the Down of a migration that cannot be reversed, said out loud rather than left nil: it refuses, with CodeMigrationIrreversible, and `<product> migrate down` below its version stops there.

func KeepVersions

func KeepVersions(n int) SecretConfigurer

KeepVersions is how many versions of a generated secret a rotation leaves, the new one included: at least 2, so that what the version it replaces sealed or signed still opens and verifies. Cover the longest-lived box or signature in flight: its lifetime divided by the interval, plus one.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Keeps

func Keeps(things ...Keeper) DatabaseConfigurer

Keeps says what the database keeps: a service's stores, a store, a module's stores. The most precise wins: a store kept by name, then its service, then its module, then the default database — the one declared without Keeps —, then the data directory, then memory. InMemory wins over all of them, on the app as on a store. The framework's data keys never live on SQLite: Keeps(app.Privacy) there keeps its holds and its journal (Privacy).

func Lease

func Lease(d time.Duration) DeliveryOption

Lease is how long a delivered message is held by its consumer before the queue hands it to another — a subscription's, a watch's, or a queued command's. The default is 30 seconds; one that is not positive stops the start. With AutoExtend, a handler that runs longer keeps it.

d is the default of the consumer's setting <service>-<kind>-<name>-lease, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Listen

func Listen(addr string) AppConfigurer

Listen sets the listening address, instead of <PRODUCT>_ADDR or PORT. ":0" picks a free port; App.URL tells which.

func Log

func Log(ctx context.Context) logger.Logger

Log returns the app's logger, for the node ctx runs inside. In dev, every record it writes inside a span is also kept beside that span, so the Studio shows a request's logs with its trace.

func Logs

func Logs(w io.Writer) AppConfigurer

Logs sends the app's logs to w instead of standard error.

func MailAttempts

func MailAttempts(n int) MailerConfigurer

MailAttempts is how many deliveries a message gets before it is dead-lettered. The default is 6, backing off from one second.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func MailMaxAge

func MailMaxAge(d time.Duration) MailerConfigurer

MailMaxAge is how long after it was queued a mail may still be retried: an attempt that fails later gives it up, as expired, whatever attempts it had left. It is the default of the mailer's setting <mailer>-max-age, which config/<env>.* and <PRODUCT>_<MAILER>_MAX_AGE override; 0 sets no age. The default is 5 days (RFC 5321 §4.5.4.1).

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func MaxBody

func MaxBody(bytes int64) EndpointConfigurer

MaxBody bounds the request body the endpoint reads. The default is DefaultMaxBody.

func MaxDeliveries

func MaxDeliveries(n int) DeliveryOption

MaxDeliveries is how many attempts a message gets before it is dead-lettered — a subscription's, or a queued command's. The default is 5.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func MaxWait

func MaxWait(d time.Duration) WaitConfigurer

MaxWait lets a call wait, at most d on the app's clock, instead of being refused at once: given to RateLimit, for a token due within d — the calls after it queue behind it —; given to Bulkhead, for a slot to free, in the queue BulkheadQueue sizes. A call still refused answers 429 or 503 as before; a caller that leaves while it waits leaves the queue. Its default is no wait, and a negative one stops the start. It is the default of the operation's setting <service>-<kind>-<operation>-rate-wait or -bulkhead-wait, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its methods are unexported — so a caller only hands it to the declaration it configures.

func Meter

func Meter(ctx context.Context) metrics.FullMeter

Meter is the product's own meter, for the app ctx runs inside: the counters, gauges and histograms it makes land in the OTLP export's metrics stream, under the app's resource and an instrumentation scope named after the app, collected every otlp-interval beside the framework's own. Without a running export — none declared, the export off, a CLI command, or outside an app — it is a meter that keeps nothing. An instrument is found again by its name and attributes, so a handler may ask for it at each call; its Collect is the export's.

func Migrations

func Migrations(m ...MigrationValue) interface{DatabaseConfigurer; ModuleConfigurer}

Migrations are migrations, the framework's own [MigrationValue]s (ADR 0179 step 10), run when the database starts (<name>-migrate: start) or by `<product> migrate up`, the framework's own first, each set under its own version table — and so its own lock. On MySQL a DDL statement commits by itself: write one per migration. The store subsystem runs them: a program that declares a database imports framework/app/store, as its engine module does.

Given to Database, they are the product's, under schema_migrations. Given to NewModule, they are the module's (ADR 0008), for the data it keeps outside the framework's stores: run on the database that keeps the module, under <module>_migrations.

func MinLength

func MinLength(n int) PasswordConfigurer

MinLength refuses a password shorter than n characters, counted as NIST counts them: one per Unicode code point. The default is 15, NIST's figure for a password used alone; 8 is the floor NIST allows with a second factor, and a smaller n is refused. There is no composition rule, ever: NIST says SHALL NOT.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Mount

func Mount(m *Module, opts ...MountConfigurer) AppConfigurer

Mount mounts module m with options: Prefix serves its routes elsewhere than "/<module>/", Bind chooses what one of its ports calls.

a := app.NewApp("vigie", identity.Service).
	With(app.Mount(moderation.Module, app.Prefix("/"), app.Bind(moderation.Enforcer, posts.EnforceAPI)))

A module is mounted once per app: mounted again, its prefix is the last one given, and its bindings join the others, the last of a port winning.

func Name

func Name(name string) EndpointConfigurer

Name overrides the endpoint's name, which otherwise is its handler's function name, or its route when the handler is a function literal.

func NewApp

func NewApp(name string, services ...*Service) *App

NewApp declares the product named name, made of services. A module's services come with the module: App.With mounts it.

func NewBinary

func NewBinary(name string, doc string) *Binary

NewBinary declares the binary named name.

func NewCommand

func NewCommand[C, R any]() *Command[C, R]

NewCommand is a command no service declares yet: Service.Command makes one and declares it, which is how a product gets one.

func NewEndpointService

func NewEndpointService[Req, Resp any]() *EndpointService[Req, Resp]

NewEndpointService is an endpoint no service declares yet: Service.Endpoint makes one and declares it, which is how a product gets one.

func NewError

func NewError(status int, code string, message string) *Error

NewError is an error answered with the HTTP status, carrying the wire code and the message a caller reads.

func NewID

func NewID(prefix string) string

NewID returns a new identifier with the given prefix: a TypeID, time ordered, like "todo_01k5zq7m3xe8tvbfg0s7zr4w6c". The prefix must be one to sixty-three lower-case letters, with '_' only between two letters; any other prefix is a programming error and panics.

func NewListener

func NewListener(contract string, handler ListenHandler) *Listener

NewListener is a listener no service declares yet, speaking contract and served by handler: Service.Listen makes one and declares it, which is how a product gets one.

func NewLoop

func NewLoop(run func(context.Context, WakeEvent) error) *Loop

NewLoop is a loop no service declares yet, whose runs are run: Service.Loop makes one and declares it, which is how a product gets one.

func NewMailer

func NewMailer() *Mailer

NewMailer is a mailer no service declares yet, with the default attempts: Service.Mailer makes one and declares it, which is how a product gets one.

func NewModule

func NewModule(name string, doc string, parts ...ModuleConfigurer) *Module

NewModule declares a module named name — lower-case letters, digits and dashes, starting with a letter; "app", "kit", "external" and "schema" are the framework's — that doc describes, made of parts: its services, which it adopts, the modules it requires (Requires), and the migrations of the data it keeps outside the framework's stores (Migrations), run on the database that keeps it. Declare it as a package-level variable of the one package a product imports:

var Module = app.NewModule("moderation",
	"Reports and moderation under the DSA.\n\nfr: Signalements et modération selon le DSA.",
	intake.Service, desk.Service, app.Requires(identity.Module))

A service a module lists is its own: what it declared so far is renamed, and what it declares later is born qualified. Listing a service links its package; a service is a module's only, and a module's services take declarations from the module's own Go module only.

func NewPortService

func NewPortService[Req, Resp any]() *PortService[Req, Resp]

NewPortService is a port no service declares yet: Service.Port makes one and declares it, which is how a product gets one.

func NewQuery

func NewQuery[Q, R any]() *Query[Q, R]

NewQuery is a query no service declares yet: Service.Query makes one and declares it, which is how a product gets one.

func NewRecordsService

func NewRecordsService(port recordsPort) RecordsService

NewRecordsService is the records of the store port reaches: App.Records and RecordsOf make one for a store the app mounts.

func NewSecret

func NewSecret() *Secret

NewSecret is a secret no service declares yet, rotated and kept as by default: Service.Secret makes one and declares it, which is how a product gets one.

func NewService

func NewService(name string, doc string) *Service

NewService declares a service. name must be lower-case letters, digits and dashes, starting with a letter; doc is one sentence saying what it owns.

func NewSettingService

func NewSettingService[T SettingValue](def T) *SettingService[T]

NewSettingService is a setting no service declares yet, holding def until something sets it: Service.Setting makes one and declares it, which is how a product gets one.

func NewStoreService

func NewStoreService[T any](key func(T) string) *StoreService[T]

NewStoreService is a store no service declares yet, keying its entities with key: Service.Store makes one and declares it, which is how a product gets one.

func NewTicker

func NewTicker(ctx context.Context, d time.Duration) clock.Ticker

NewTicker is a ticker of period d on the app's clock, for a loop written by hand (Service.Go): a test's manual clock drives it like every timer the framework runs itself, where a time.Ticker would tick on the wall clock whatever the test does. It panics on a non-positive d, as time.NewTicker does.

func NewTopicService

func NewTopicService[T any]() *TopicService[T]

NewTopicService is a topic no service declares yet: Service.Topic makes one and declares it, which is how a product gets one.

func NewWorkflowService

func NewWorkflowService[E any, S comparable](store *StoreService[E], state func(*E) *S) *WorkflowService[E, S]

NewWorkflowService is a workflow no service declares yet, over the entities of store whose state state reads: Service.Workflow makes one and declares it, which is how a product gets one.

func NoRetryAfter

func NoRetryAfter() RetryConfigurer

NoRetryAfter makes a Retry wait its backoff whatever a failure says: a Retry-After, a rate limiter's next token. It is the default of the port's setting <service>-port-<name>-retry-after, false, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func NoTransaction

func NoTransaction() CommandConfigurer

NoTransaction runs a command outside a transaction of its own: its writes land as its handler makes them, and its effects leave when made — a failed command may have written or announced something. On the data directory and in memory, where a writing command's transaction takes the writer turn, it lets such commands run side by side. Dispatched inside another command, it still runs in that one's transaction.

func NotCommon

func NotCommon() PasswordConfigurer

NotCommon refuses a password among the ten thousand most common ones — the SDK's password.IsCommon, which compares them without regard to case —: the blocklist NIST SP 800-63B-4 requires a verifier to check, beyond the three thousand OWASP ASVS 5.0 (6.2.4) asks for. Every policy checks it (ADR 0007 §2: on by default once the SDK has the list); the option says so where a policy is declared. Beside NIST's fifteen characters the list has little left to refuse; beside a shorter MinLength, it matters.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func NotFound

func NotFound(message string) *Error

NotFound reports that the addressed resource does not exist (404).

func NotReused

func NotReused(n int) PasswordConfigurer

NotReused refuses a password that the current hash, or any of the n former ones, verifies — n from 1 to 100. It is a compliance policy, off by default: PCI DSS asks for it, OWASP ASVS 4.0.3 asks most products not to impose it. The framework keeps the field's last n hashes, so it implies history=n, and a tag whose history is smaller than n is refused.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Now

func Now(ctx context.Context) time.Time

Now returns the app's time: the system clock, or the clock a test gave with Clock. Product code that compares with a deadline reads it rather than time.Now, so that a test's manual clock reaches it.

func OTLP

func OTLP(opts ...OpenTelemetryConfigurer) AppConfigurer

OTLP declares that the app forwards its traces and the metrics the framework derives from them to an OpenTelemetry collector over OTLP/HTTP: the spans the framework starts — the share otlp-sample-ratio keeps —, and per node and operation its runs, failures and durations (app.node.runs, app.node.failures, app.node.duration), with what the export itself holds and lost (app.otlp.*) and the product's own instruments (Meter). The export is on once its endpoint is set — the setting otlp-endpoint, OTLPEndpoint its default — or once OTLPSender replaces the network; a CLI command never exports. A program that declares it imports framework/app/otlp, or its start is refused, naming the import:

import _ "github.com/kitsunium/sdk/framework/app/otlp"

var App = app.NewApp("shop", catalog.Service).With(
	app.OTLP(app.OTLPEndpoint("http://collector:4318")),
)

Every value given here is the default of the setting that tunes it, which config/<env>.* and <PRODUCT>_OTLP_<KNOB> override. Given twice, the last wins.

func OTLPBounds

func OTLPBounds(maxBatches int, maxBytes int) OpenTelemetryConfigurer

OTLPBounds bounds each signal's queue: at most maxBatches batches and maxBytes bytes — the defaults of the settings otlp-max-batches (10000) and otlp-max-bytes (256 MiB). A batch larger than maxBytes on its own is dropped.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPDatabase

func OTLPDatabase(name string) OpenTelemetryConfigurer

OTLPDatabase keeps the export's queues on the database the app declares under name — the default of the setting otlp-database —, one table per signal (app_otlp__traces, app_otlp__metrics), made by the framework's own migrations: a backlog then outlives a restart with no data directory — a container with no volume — and on the platforms whose file queue the SDK refuses. It needs framework/app/queue, which the start names when the program does not import it; a name the app does not declare stops the start. The empty default keeps the queues in the data directory, else in memory, and so does a declared database that is not open — the app in memory, or in dev without its URL —, which the log says.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPEndpoint

func OTLPEndpoint(url string) OpenTelemetryConfigurer

OTLPEndpoint is the collector's base URL — "http://collector:4318" —, each signal's path (/v1/traces, /v1/metrics) appended to it: the default of the setting otlp-endpoint. An absolute http or https URL with a host; the empty default leaves the export off.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPMaxAge

func OTLPMaxAge(d time.Duration) OpenTelemetryConfigurer

OTLPMaxAge is how long after it was queued a batch may still be sent — the default of the setting otlp-max-age (24h): one older is dropped unsent, counted expired. A collector's Retry-After is waited in full, but never past it.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPSampleRatio

func OTLPSampleRatio(ratio float64) OpenTelemetryConfigurer

OTLPSampleRatio is the share of the traces the app starts that the export keeps — the default of the setting otlp-sample-ratio (1, every one): from 0, none, to 1, every one, decided on the trace's identifier, so every service that sees the same trace decides alike. A trace another process started is kept or not as that process decided, whatever the ratio: a span is never cut off from its parent. The metrics the framework derives count every unit of work, kept or not.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPSendTimeout

func OTLPSendTimeout(d time.Duration) OpenTelemetryConfigurer

OTLPSendTimeout bounds one attempt to send a batch — the default of the setting otlp-send-timeout (10s, OTEL_EXPORTER_OTLP_TIMEOUT's).

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPSender

func OTLPSender(send func(ctx context.Context, batch OTLPBatchValue) error) OpenTelemetryConfigurer

OTLPSender replaces the network: each batch is handed to send instead of being posted to a collector — the product's own sender, a bridge to another transport. The queue, its bounds, its age and its retries stay the framework's. A failure send returns is tried again on the backoff — after the delay it carries when it is a resilience.RetryAfter's —, but for a refusal the caller must fix, an Error of a 4xx other than 408 and 429, which dead-letters the batch. The export is then on whatever otlp-endpoint says. A nil send keeps the network.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPWhenFull

func OTLPWhenFull(p OTLPFullPolicy) OpenTelemetryConfigurer

OTLPWhenFull is what a full queue does with a new batch — the default of the setting otlp-when-full: OTLPDropOldest (the default), OTLPDropNewest or OTLPBlock.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OnBounce

func OnBounce(fn func(ctx context.Context, b *BounceEvent)) MailerConfigurer

OnBounce replaces what the mailer does with a mail it gives up — by default, it logs it as an error naming why — with fn: mark an address undeliverable, tell an operator, send it another way. fn runs on a goroutine of its own once the mailbox is updated — the outbox goes on meanwhile, and fn may send mail —, with a context on the mailer's node that continues the trace of the Send; the mailer's stop waits for it. A panic in fn is logged and changes nothing else. A nil fn keeps the framework's own.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Only

func Only(envs ...string) MountConfigurer

Only makes a mount hold in the environments envs names alone — the name <PRODUCT>_ENV or Env gives, lower-cased, "production" when neither does:

a := app.NewApp("vigie", identity.Service).
	With(app.Mount(debugbar.Module, app.Only("dev", "staging")))

Elsewhere the mount is as if it had not been given, its Prefix and its Bind with it; another mount of the module without Only still mounts it. Several Only on one mount add up. The start refuses an Only that names no environment, a name that cannot be one — lower-case letters, digits and dashes inside, 63 at most —, and a module a mounted module requires (Requires) that its mount leaves out, rather than mounting it at its defaults behind the product's back.

func OpenBackoff

func OpenBackoff(maxOpen time.Duration) BreakerConfigurer

OpenBackoff lengthens the open time of a Breaker that opens again without having closed: it doubles at each opening, from the breaker's open time up to max. It is the default of the port's setting <service>-port-<name>-breaker-open-max, 0 — the same open time every time — without it; a negative one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Optional

func Optional() SecretConfigurer

Optional makes a provided secret one the product can do without — the key of a feature that stays off while nobody gives it. A start where it is set nowhere goes on; Secret.Present says whether it is set now, and a use of it while it is absent — Secret.Value, Seal, Open, Sign or Verify — answers an error errs.HasCode matches with CodeSecretMissing. It is read where it lives at every use, as every provided secret is: given later — `secrets set` in the environment's store, a new content in the file its _FILE variable names —, it is found at its next use. A generated secret is the framework's to make and never absent: Optional with Generated is refused.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Overlap

func Overlap(p OverlapPolicy) JobConfigurer

Overlap says what a fire of the job does while its previous run is still going: OverlapSkip, the default, skips it and counts it; OverlapAllow runs it beside; OverlapReplace cancels the previous run's context and runs it once that run returned. Any other word stops the start.

p is the default of the job's setting <service>-job-<name>-overlap, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OwnStores

func OwnStores() SubscriptionConfigurer

OwnStores lets a watch (Service.Watch) hear the stores of its own module too — the product's own, for a watch of the product's —, which a watch never hears by default. Such a watch never hears the writes its own handler makes, in its handler's context — a handler that writes what it watches, a screening that stamps the record it screened, would otherwise hear itself forever —; a write the handler only causes, later and elsewhere — a queued command's handling, a topic's delivery — is heard. A subscription to a topic refuses it: a topic has no stores.

func Parallelism

func Parallelism(n int) DeliveryOption

Parallelism is how many messages are handled at once — a subscription's, or a queued command's. The default is 1.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func PerEnv

func PerEnv(name string, fallback string) ScopeValue

PerEnv is the environment variable name, else fallback when it is unset or empty; a leading "/" in either is the user's home directory. The status line's is PerEnv("CLAUDE_CONFIG_DIR", "/.claude"): one daemon per Claude configuration.

func Prefix

func Prefix(prefix string) MountConfigurer

Prefix serves a module's routes under prefix — "/trust/" — instead of "/<module>/"; "/" shares the product's route space, where a route both declare is refused as any two are. A prefix starts and ends with '/', and never lies under /_app/, nor under /_kit/ while its probes answer there.

func Profile

func Profile(p string) AppConfigurer

Profile selects the app's process profile: model.ProfileServer (the default), model.ProfileDaemon or model.ProfileCLI.

func Purpose

func Purpose(text string) StoreConfigurer

Purpose says why the store keeps its records (GDPR art. 30(1)(b)): the register lists it, and a person's export carries it.

func Queued

func Queued() CommandConfigurer

Queued sends a command to the background. Dispatch returns once the command's own queue accepted it — a file queue under the data directory, memory without one —, and a consumer handles it, Parallelism at a time, retried up to MaxDeliveries attempts (5) — half a second after the first failure, the wait doubling after each, never more than a minute —, then dead-lettered; a dispatch that does not decode is dead-lettered at its first failure, since no retry makes it decode. It answers nothing: its result is EmptyValue, and exposed, it answers 202 Accepted.

Delivery is at least once: Queued is the product's promise that the handler tolerates a redelivery, and a Command.Key narrows the duplicates without removing them. The handler runs in the dispatcher's trace, as the user who dispatched it (UserID); what the auth handler said about that user is not queued. A queued command's input is a contract with the messages already queued: add a field, never rename one.

func RateLimit

func RateLimit(perSecond float64, burst int, opts ...RateLimitConfigurer) OperationOption

RateLimit puts a token bucket in front of the handler: perSecond tokens a second, up to burst at once. An empty bucket answers 429 at once — with Retry-After, the seconds the bucket needs to earn its next token —, or, given MaxWait, once the token due within it did not come. It is the SDK's resilience rate limiter.

The values are the defaults of the operation's settings <service>-<kind>-<operation>-rate, -burst and -rate-wait, which config/<env>.* and <PRODUCT>_<NAME> override; a rate that is not above zero or a burst below one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RateLimitPerClient

func RateLimitPerClient(perSecond float64, burst int) OperationOption

RateLimitPerClient gives every client of an endpoint a token bucket of its own: perSecond tokens a second, up to burst at once. A client is the authenticated user when there is one, and the client's address otherwise (ClientIP); in-process calls through EndpointService.Call share one bucket. One client emptying its bucket is answered 429, with Retry-After; the others are not — which is what a sign-in endpoint needs, where one shared bucket would let a single client lock everybody out.

Buckets unused for ten minutes are forgotten, and at most 10 000 are kept: the least recently used goes first. It is the SDK's keyed rate limiter.

The values are the defaults of the operation's settings <service>-<kind>-<operation>-client-rate and -client-burst, which config/<env>.* and <PRODUCT>_<NAME> override; a rate that is not above zero or a burst below one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func ReadModel

func ReadModel() StoreConfigurer

ReadModel marks a store as derived from others (ADR 0005): written by projections — the subscriptions that keep it from the write side's events —, read by queries. It changes nothing of how the store runs; the diagram draws it on its domain's read side, and the static analysis warns of a read model written by anything but a subscription. The framework's topics are queues, not logs: a read model cannot be replayed from its events, and one fed by a topic lags the commands that feed it — a query that must read its own writes reads the write side.

var Summaries = Service.Store("summaries", Summary.Key, app.ReadModel())

func ReadOnly

func ReadOnly() TransactOption

ReadOnly opens the transaction read-only: the database is told it only reads, and a store's write inside it is refused (Invalid, CodeTransactionReadOnly) on every backend, before it reaches one. Given to a query's declaration, it runs the query's Ask in such a transaction — the caller's own, when the Ask runs inside one.

func RecordsOf

func RecordsOf(ctx context.Context, store string) (RecordsService, error)

RecordsOf is App.Records for code the app runs and that does not hold it — a module's watch, its endpoints: the records port of the store whose node ID is store, in the app ctx runs in. Outside a running app it is an Unavailable error.

func Screen(ctx context.Context, w app.Written) error {
	recs, err := app.RecordsOf(ctx, w.Store)
	…
	record, err := recs.Get(ctx, w.Key)

func RegisterConfigFormat

func RegisterConfigFormat(ext string, format string)

RegisterConfigFormat lets configuration files of extension ext be read with the SDK codec format. framework/app/config/yaml and config/toml call it as they are imported; a product never does.

func RepeatableRead

func RepeatableRead() TransactOption

RepeatableRead opens the transaction at the REPEATABLE READ isolation level: a row it read reads the same until it ends. On the data directory and in memory it changes nothing.

func Replace

func Replace[Req, Resp any](op Operation[Req, Resp], fn func(context.Context, Req) (Resp, error)) AppConfigurer

Replace runs fn in place of op's code, in the app it is given to: an endpoint's handler — or, with ADR 0005, a command's or a query's — or what a port calls. It replaces the code, not the mechanics: authentication, decoding, validation and the policies still run, and a replaced port needs no binding, so a module's tests run without a host. fn takes op's request and returns its response, or it does not compile.

A replaced run's span carries mock: replace, and runtime.mocks lists the replacement. It is for tests: the start refuses it in a program go test did not build — neither a production binary nor kit dev can carry one. Given twice for one operation, the last wins.

app := App.With(app.Replace(apps.AppAPI, func(ctx context.Context, in apps.IDInput) (apps.Profile, error) {
	return apps.Profile{ID: in.AppID}, nil
}))

func Required

func Required() SettingConfigurer

Required drops a setting's default: a start without a value for it fails, saying where to give one.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Requires

func Requires(other *Module) ModuleConfigurer

Requires says the module needs other: an app that mounts the module and not other mounts other at its defaults, and starts it first. The Go import this takes makes a cycle impossible. A collaboration a module can do without is a port with a fallback instead (Fallback).

func RetentionByProduct

func RetentionByProduct(limits string) StoreConfigurer

RetentionByProduct says the product keeps the store's retention itself: its own code erases and deletes what it no longer needs — a record whose life the product's own rules end, a lifecycle the framework cannot read from the entity. limits says, in the product's words, for how long and how, which the register publishes as the store's time limits (GDPR art. 30(1)(f)); "" says only that the product keeps them.

The framework then runs no retention for the store, and warns of neither a missing retention nor a missing subject. Its classified fields keep every other promise: sealed at rest, never shown, exported with their person — when the store names one — and erased by app.Erase and Store.Erase. It goes with app.Purpose, app.HeldUntil, app.Anonymise and app.DeleteOnErasure; with app.EraseAfter, app.EraseAt, app.DeleteAfter or app.DeleteAt it is refused: a store's retention is the framework's or the product's, never both.

func Retry

func Retry(maxAttempts int, delay time.Duration, opts ...RetryConfigurer) PortPolicyConfigurer

Retry runs a port's call again when it fails for a reason of the dependency's — what the start answers as a 5xx, a 408 or a 429 —, up to maxAttempts attempts in all, waiting between them a backoff that starts at delay and doubles, with full jitter, up to RetryMaxDelay — 30 seconds unless the code says otherwise. A failure that says how long to wait — a Retry-After, a rate limiter's next token (the SDK's resilience.RetryAfter) — is waited that long instead, held at the longest delay, unless NoRetryAfter. A refusal of the caller's own is not retried, nor an open breaker, a throttle or another retry's exhaustion. It is the SDK's resilience retry, on the app's clock, sharing the port's retry budget (Budget) with its hedge.

Each retry is an event on the port's span, which counts them, and a retry event on the telemetry port.

The values are the defaults of the port's settings <service>-port-<name>-retry-attempts, -retry-delay, -retry-max-delay, -retry-jitter (full, decorrelated or none) and -retry-after (whether a failure's own delay is honoured), which config/<env>.* and <PRODUCT>_<NAME> override; fewer than one attempt, or a delay that is not positive, stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RetryIn

func RetryIn(d time.Duration, cause error) error

RetryIn marks cause as a failure to retry in d, no sooner: returned from the handler of a subscription, a watch or a queued command, it hands the message back to its queue with that delay, in place of the queue's own — half a second after a first failure, doubling, never more than a minute —: a downstream that answered with a Retry-After, a resource that frees at a known instant. The delivery counts as an attempt: a message that used its MaxDeliveries is dead-lettered with cause, the delay unused. A negative d is read as zero.

if resp.StatusCode == http.StatusTooManyRequests {
	return app.RetryIn(30*time.Second, errThrottled)
}

cause keeps its own code, reason and words — what a dead letter records — and CodeRetryIn joins its trail; a cause that is no error of the SDK's, or none, is recorded as CodeRetryIn itself. Outside a consumer's handler it is an error like any other.

func RetryMaxDelay

func RetryMaxDelay(d time.Duration) RetryConfigurer

RetryMaxDelay is the longest a Retry waits between two attempts: the backoff's cap, and the cap on a delay a failure asks for. It is the default of the port's setting <service>-port-<name>-retry-max-delay; one that is not positive stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Revisions

func Revisions(n int) StoreConfigurer

Revisions keeps the n previous versions of each record of the store, n from 1 to 100, as WordPress keeps a post's revisions: every write that changes a record makes a version, numbered from 1 and never renumbered, stamped with when it was made, the caller's user and the command that made it; the oldest beyond n are pruned by the write that makes a newer one, unless a legal hold keeps the record.

var Pages = Service.Store("pages", Page.Key, app.Revisions(20))

revs, err := Pages.Revisions(ctx, id)                            // newest first
edits, err := Pages.Diff(ctx, id, revs[3].Number, revs[0].Number)
page, err := Pages.Restore(ctx, id, revs[3].Number)              // a new version
page, err = Pages.Restore(ctx, id, revs[3].Number, "/title")     // the title only

A store with Revisions(n) holds up to n+1 times its records: in memory with the store in the data directory, in the table <table>___vs on a database, which the framework's migrations create.

func Root

func Root(dir string) StaticConfigurer

Root serves the sub-directory dir of the file system rather than its root — "assets" for a //go:embed assets directive.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RotateEvery

func RotateEvery(d time.Duration) SecretConfigurer

RotateEvery is how often the framework makes a new version of a generated secret.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func ScopeKey

func ScopeKey(scopes ...ScopeValue) (string, error)

ScopeKey is the name the scopes give in this process: 16 hex digits of the SHA-256 of their values. A client computes the key the daemon computed from the same declaration, and so finds its lock or its socket.

func SecretStore

func SecretStore(store secret.Store) AppConfigurer

SecretStore keeps the secrets in store instead of the environment's: a test gives a store holding what its provided secrets hold. The environment's variables are still read first.

func Serializable

func Serializable() TransactOption

Serializable opens the transaction at the SERIALIZABLE isolation level: it commits only if running it alone would have given the same result, and a concurrent transaction that would break that fails instead — on PostgreSQL with SQLSTATE 40001, which the database asks to run again. On the data directory and in memory, whose transactions already take one writer at a time, it changes nothing.

func Set

func Set(name string, value any) AppConfigurer

Set gives the setting name a value in code, over the environment and the files: a value of the setting's type, or its text as a variable would hold it. It is for tests.

func SetCookie

func SetCookie(ctx context.Context, c *http.Cookie)

SetCookie adds a Set-Cookie header to the HTTP response of the endpoint ctx serves. The framework fills what a cookie left zero with safe defaults — Path "/", HttpOnly, SameSite Lax, and Secure outside dev — so a session cookie is hardened unless the code says otherwise. In an in-process EndpointService.Call there is no response: the cookie is dropped.

HttpOnly is always set, and Secure always outside dev (and in dev for SameSite=None, which browsers require): a boolean has no "unset" a default could fill, and the framework chooses the hardened reading. A cookie set more than once in one response keeps its last value, and cookies set before an error are sent with the error — clearing a stale session on a 401 works.

func Settle

func Settle(d time.Duration) DeliveryOption

Settle bounds each outcome a consumer sends once the app began to stop — an acknowledgement, a nack, a message handed back — and each renewal of a lease (AutoExtend): a queue that stopped answering cannot hold the stop. The default is 5 seconds; one that is not positive stops the start.

d is the default of the consumer's setting <service>-<kind>-<name>-settle, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Singleton

func Singleton(scope string) AppConfigurer

Singleton keeps one process of the app alive per scope on this machine: the start takes an exclusive file lock named after scope in the app's runtime directory (pkg/v1/app/lock: flock, LockFileEx) and holds it until the process ends. A second process refuses to start with CodeSingletonHeld — for a daemon's client, the sign to talk to the one that runs. The kernel drops the lock with a dead process.

func SingletonPer

func SingletonPer(scopes ...ScopeValue) AppConfigurer

SingletonPer keeps one process of the app alive per value of scopes on this machine — per user, per installed executable, per configuration directory (PerUID, PerExecutable, PerConfigDir, PerEnv) —: the lock is named after the scopes' key (ScopeKey), computed at the start, and held as Singleton's is. A client that must find the process computes the same key from the same scopes.

func SocketPath

func SocketPath(path string) ListenerConfigurer

SocketPath places the listener's socket at path — absolute, at most 103 bytes — instead of <runtime dir of the app>/<service>-<name>.sock.

IFACE-OPAQUE: ListenerOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to Listen.

func SocketPathFor

func SocketPathFor(product string, service string, name string, scopes ...ScopeValue) (string, error)

SocketPathFor is the socket of the listener name of service in product — the binary's name for a role of one, else the app's —, placed by default and named after scopes (SocketPer): the one rule, for a client of another process role that does not import the daemon's declarations (D22) — it names the three words and the scopes its contract states. The service is its qualified name for a module's.

func SocketPer

func SocketPer(scopes ...ScopeValue) ListenerConfigurer

SocketPer names the listener's socket after the value of scopes in this process — <runtime dir of the app>/<service>-<name>-<key>.sock, the key being ScopeKey's —: one daemon per user, per executable, per configuration directory. A client finds it with SocketPathIn or DialIn, which compute the same key from the same declaration.

IFACE-OPAQUE: ListenerOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to Listen.

func StartingDeadline

func StartingDeadline(d time.Duration) JobConfigurer

StartingDeadline bounds how late a fire of the job may still start: a scheduler that comes to a fire more than d after its instant — a machine that slept, a run that held the slot — does not run it, and counts it missed, as Kubernetes' startingDeadlineSeconds does. The default, zero, is no bound: the latest fire missed runs however late, never more than one. A negative d stops the start.

d is the default of the job's setting <service>-job-<name>-starting-deadline, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Statements

func Statements(stmts ...string) MigrationStep

Statements is the MigrationStep that runs stmts in order on its executor, stopping at the first that fails — whose error it returns as the driver said it.

func Stop

func Stop(ctx context.Context) bool

Stop asks the app ctx runs in to end its run: Run returns as it does on a signal, its stop draining every component — a daemon told "stop" by its client, on its listener, ends itself so. It returns at once, before the stop; it reports false when ctx runs in no app — a handler's, a loop's, a command's context does — or the app does not run.

func Studio

func Studio(on bool) AppConfigurer

Studio switches the Studio on or off in dev. It is always off in production.

func Telemetry

func Telemetry(path string, gids ...int) AppConfigurer

Telemetry exports the app's telemetry on the private socket at path — absolute, and short enough for a Unix socket (pkg/v1/proc/ipc) — admitting the groups gids besides the product's own account. It wins over <PRODUCT>_TELEMETRY.

func Throttle

func Throttle(k float64) PortPolicyConfigurer

Throttle refuses, on the caller's side and without sending them, a share of a port's calls while the dependency refuses more than 1/k of the recent ones — the client-side throttling of Google's SRE book: a call is refused with probability (requests − k × accepts) / (requests + 1), over the last two minutes, with the SDK's Throttled (503). A refusal is a 5xx, a 408 or a 429 of the dependency's; a refusal of the caller's own is an accept. It is the SDK's resilience adaptive throttle, on the app's clock; 2 is the book's k.

k is the default of the port's setting <service>-port-<name>-throttle-k, which config/<env>.* and <PRODUCT>_<NAME> override; a k below 1 stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Timeout

func Timeout(d time.Duration) TimeoutConfigurer

Timeout cancels the handler's context after d and answers 504. A caller that leaves first is no timeout: its request is recorded as 499, logged as information, and nothing it could read is written. It is the SDK's resilience timeout, on the app's clock.

Given to a port's PortService.Guard, it is a deadline on each attempt of the port's call, the innermost of its policies: an attempt that outruns it fails with the SDK's TimeoutExceeded, which the port's breaker counts and its retry replays.

d is the default of the operation's setting <service>-<kind>-<operation>-timeout — <service>-port-<name>-timeout for a port —, which config/<env>.* and <PRODUCT>_<NAME> override; one that is not positive stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Transact

func Transact(ctx context.Context, fn func(context.Context) error, opts ...TransactOption) error

Transact runs fn in one transaction and returns fn's error: nil commits what fn wrote, an error undoes it — a panic too, which then continues.

err := app.Transact(ctx, func(ctx context.Context) error {
	if _, err := Cases.Update(ctx, id, closeCase); err != nil {
		return err // what this function wrote is undone
	}
	if err := Decisions.Insert(ctx, d); err != nil {
		return err
	}
	return Changes.Publish(ctx, Closed{ID: id}) // held: queued at the commit
})

A transaction belongs to one database: the one its first store call reaches — at its first call on a database, at its first write on the data directory, which counts as one, memory with it. Inside it, a store of another database is read as usual; writing one is refused (Invalid, CodeTransactionSpan). A Transact inside another is a savepoint: its failure undoes its own writes, and the outer transaction goes on if its caller handles the error.

What leaves the process waits for the outermost commit: a topic's publish, a queued command's dispatch, a mail — whose outbox ID Mailer.Send returns at once —, a watch's notice, the store's write hooks that wake workflows and loops, a workflow's OnTransition hooks. Each is checked when it is made, so a caller's mistake returns to the caller; a rollback drops them, and a savepoint's rollback its own. A publish, a dispatch, a mail or a notice whose queue lives on the transaction's database — the one that keeps the service of its subscription, command, mailer or watch, by a store's rules — is written in the transaction, just before it commits, a mail sealed under the framework's data-key: a queue that refuses it fails the transaction, every write undone, and Transact returns the refusal. Every other one leaves after the commit: one a queue refuses then is logged and reported, and the commit stands; a process that dies between the commit and the release loses what was held.

On a database the transaction is the database's own, at its default isolation unless opts ask for another: Serializable or RepeatableRead opens it at that level, ReadOnly tells the database it only reads, and a store's write inside it is refused (Invalid, CodeTransactionReadOnly) on every backend. Options go on the outermost Transact: a nested one given any is refused (Invalid, CodeTransactionNested), its savepoint able to change neither. On the data directory and in memory the framework keeps its own: it takes the data's writer turn — one transaction at a time; a write outside any waits for it, and such writes share the turn among themselves —, its writes land at once, and an error writes each entity's previous value back, newest first. It is atomic against an error, not against a crash, and a reader may see a write before its commit. A store kept in memory by InMemory — a cache — is in no transaction: its writes land and stay.

A command's handler runs in one already (NoTransaction opts out), and so does a workflow's transition.

func Unauthenticated

func Unauthenticated(message string) *Error

Unauthenticated reports a caller who did not prove who they are (401).

func Unavailable

func Unavailable(message string) *Error

Unavailable reports a transient failure worth retrying later (503).

func Unique

func Unique[T any](name string, key func(T) string) StoreConfigurer

Unique declares a unique index: no two entities may share a key, and a write that would is refused with a Conflict error. An empty key is not indexed. StoreService.Lookup reads it.

var Accounts = Service.Store("accounts", Account.Key,
	app.Unique("email", func(a Account) string { return a.Email }))

func UserAgent

func UserAgent(ctx context.Context) string

UserAgent returns the User-Agent of the request ctx serves, clipped.

func UserID

func UserID(ctx context.Context) (UID, bool)

UserID returns the authenticated caller of the request ctx serves, and whether there is one. An in-process EndpointService.Call carries its caller's user along.

func WakeAt

func WakeAt(next func(context.Context) (time.Time, bool)) LoopConfigurer

WakeAt wakes the loop at the time next returns, asked again after every run; false means "no deadline for now". It is how a loop sleeps exactly until its next piece of work — the next reminder due — rather than polling.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func WakeEvery

func WakeEvery(d time.Duration) LoopConfigurer

WakeEvery wakes the loop every d.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func WakeOn

func WakeOn[T any](topic *TopicService[T]) LoopConfigurer

WakeOn wakes the loop whenever a message is published on topic. It is a nudge, not a subscription: the loop is not handed the message and wakes once for a burst — it re-reads the state it works on.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func WithUser

func WithUser[D any](ctx context.Context, uid UID, data D) context.Context

WithUser returns ctx acting as the given user, with data: what a job, a loop or a test uses to call an Auth endpoint in-process on someone's behalf.

type ActivityHandler

ActivityHandler is a declared activity of a service: a function that says whether the product is busy. A daemon's IdleStop waits until every activity of its mounted services says false AND no connection is open on its listeners.

type ActivityHandler struct{}

type AnalyzeFunc

AnalyzeFunc reads the product's source — the Go module rooted at dir, and the packages that declare the modules it mounts — into a graph the runtime merges into its own: the edges found in handler bodies, each function's steps. The framework links no analyzer (ADR 0147 §1): the platform's former runtime tool gives one, in dev, through Analyzer.

type AnalyzeFunc func(ctx context.Context, dir string, modules []string) (*model.Graph, error)

type App

App is the whole product: the services it mounts, served by one process. The same services can be mounted by several apps — one binary per service, or all of them in one — and each app draws exactly what it runs.

type App struct{}

func (App) Fields
func (*App) Fields(m Mark) []FieldRefValue

Fields lists the fields of the app's stores that carry the mark, store by store in the order the app mounts them, each in its entity's order. Personal lists special fields too: they are personal data.

func (App) Graph
func (*App) Graph() *model.Graph

Graph returns the product as a diagram: every node the app mounts with the position of its declaration, the edges that exist by construction, the edges observed at runtime with their counters, and — in dev, once the static analysis has run — the edges found in the handlers' code with their call sites. It is what the Studio draws and what `<app> graph` prints.

func (App) Handler
func (*App) Handler() http.Handler

Handler returns the app's HTTP handler, once started: what httptest wraps.

func (App) Main
func (*App) Main(ctx context.Context, args []string) int

Main is the whole main function of a product:

func main() { os.Exit(App.Main(context.Background(), os.Args[1:])) }

It understands ten commands, so every binary of the framework can describe itself, be probed, say its configuration, manage its secrets, migrate its databases, answer for the personal data it keeps, replay what its watches hear and copy its data directory into a database without a shell or another tool in its image:

serve        run the product (the default)
graph        print the product graph: -format json|mermaid
healthcheck  exit 0 when the product running on <PRODUCT>_ADDR is ready
config       every setting: its value, never a secret's, and its origin
secrets      list the secrets, set one from standard input, rotate one
migrate      the databases' migrations: status, up, down SET VERSION
privacy      the register of processing, a person's export and erasure,
             the legal holds, the privacy journal, the retention
revisions    a record's version restored
watch        replay WATCH: one notice per record of each store that feeds it
data         the data directory copied into a database: data copy --to NAME,
             verified by fingerprints

An app whose port declares a Breaker understands breaker too: list the breakers, isolate PORT — open its breaker until it is reset — and reset PORT, kept in the data directory for every start and a running product. An app that declares an OTLP export understands otlp: dead-letters lists the batches its collector refused for good, never their payload, and replay puts them back into the live queue, or with --drop deletes them.

A product's CLI command runs when the first argument names it. A command declared with DefaultCommand runs when the first argument names no command at all — with every argument —, and when there is none, in serve's place: Main then never answers a usage error.

It returns the process exit status rather than exiting, so that deferred work runs and a test can call it.

func (App) Name
func (*App) Name() string

Name returns the product name.

func (App) Records
func (*App) Records(store string) (RecordsService, error)

Records returns the untyped port to the records of the store whose node ID is store: what the framework's export and erasure run on, and what a module reads and erases through. A store the app does not mount is a NotFound error.

func (App) Run
func (*App) Run(ctx context.Context) error

Run starts the product, serves until ctx ends or the process receives SIGINT or SIGTERM, then drains it.

Goroutine lifecycle: one goroutine turns a signal into the run's cancellation; it ends when a signal arrives or when the run returns, which cancels the context it also watches.

func (App) Start
func (*App) Start(ctx context.Context) error

Start brings the product up: it checks every declaration, opens the data directory, loads the stores, starts the consumers, the scheduler and the HTTP listener — in that order, through the SDK lifecycle, so a failure unwinds what already started. It returns once the product serves.

func (App) Stop
func (*App) Stop(ctx context.Context) error

Stop drains the product: readiness turns false, then the components stop in reverse order — the HTTP listener finishes its requests before the stores close.

func (App) URL
func (*App) URL() string

URL returns the base URL the app listens on, once started.

func (App) With
func (*App) With(opts ...AppConfigurer) *App

With returns a new app: the same product, with the options of a and then opts — a module among them is mounted. a itself is left as it was, so the options a test gives the product's App — an app.Set, an app.Replace — end with that test.

type AppConfigurer

AppConfigurer configures an app. Options win over the environment.

type AppConfigurer interface {
	appConfigure(o *appOptions)
}

type AttrsProvider

AttrsProvider is what the app's auth data says about the caller to the SDK's authz: the attributes Command.Allow and Query.Allow hand a policy — the caller's roles, its groups. Auth data that does not implement it gives none, and every Allow refuses: the start warns of it.

type Me struct{ Roles []string }

func (m Me) Attrs() []authz.Attr { return []authz.Attr{authz.AttrStrings("roles", m.Roles...)} }

The framework calls the caller a principal: in the framework, a subject is the person personal data is about (ADR 0006).

type AttrsProvider interface {
	Attrs() []authz.Attr
}

func (AttrsProvider) Attrs
func (AttrsProvider) Attrs() []authz.Attr

type AuthenticatorHandler

AuthenticatorHandler is the app's authentication handler: it turns the credentials of a request — a session cookie, a bearer token — into a user, in front of every endpoint declared with Auth or AuthOptional. An app has at most one.

P is the credentials: a struct whose fields are tagged cookie:"…" or header:"…", decoded like an endpoint's request. D is what the handler says about the user; endpoints read it with AuthData.

type AuthenticatorHandler[P, D any] struct{}

type Binary

Binary is one executable the product ships, made of process roles: a status line and its daemon are two roles of one binary, "statusline" and "statusline daemon". Each role is an App — its own services, its own profile — and two roles talk only through a declared, versioned contract (D22): a listener one serves and the other dials, never each other's code.

var Binary = app.NewBinary("statusline", "The status line of a Claude session.").
	Role("render", RenderApp).                // statusline ...
	Role("daemon", DaemonApp, "daemon").      // statusline daemon ...
	Talks("render", "daemon", "render/v1")

func main() { os.Exit(Binary.Main(context.Background(), os.Args[1:])) }

The package that declares a binary and its roles is wiring, generated from the design (D22).

type Binary struct{}

func (Binary) Main
func (*Binary) Main(ctx context.Context, args []string) int

Main runs the role the arguments select — the one whose args are the longest prefix of them, else the default role — with the rest of the arguments, through its App's Main. It returns the process exit status.

func (Binary) Role
func (*Binary) Role(name string, app *App, args ...string) *Binary

Role adds a process role named name, run by app, selected when the process's first arguments are args; the role without args is the default. It returns b, so roles chain.

func (Binary) Talks
func (*Binary) Talks(from string, to string, contractName string) *Binary

Talks declares that role from talks to role to through contract — "<name>/v<major>". It is the only edge two roles may have.

type BounceEvent

BounceEvent is a mail its mailer gave up on: the receiving server refused it for good, it outlived its mailer's max-age, or its last attempt failed — GivenUp says which. It stays in the outbox as a dead letter.

Reply is the receiving server's own answer: the hook may read it, log it or act on it, but the framework never puts it in a message it shows — the Studio's mailbox keeps its code alone.

type BounceEvent struct {
	// Err is the last attempt's failure.
	Err      error
	// Reply is the SMTP reply that failure carries, nil when the server
	// answered none — a relay that could not be reached, a timeout.
	Reply    *textproto.Error
	// Message is the mail, as it was queued.
	Message  mail.Message
	// QueuedAt is when it entered the outbox.
	QueuedAt time.Time
	// At is when it was given up, on the app's clock.
	At       time.Time
	// Mailer is the mailer's node ID.
	Mailer   string
	// ID is the mail's outbox ID.
	ID       string
	// GivenUp is why: model.MailRejected, model.MailExpired or
	// model.MailExhausted.
	GivenUp  string
	// Attempts is how many deliveries it got.
	Attempts int
}

type BreakerConfigurer

BreakerConfigurer configures a Breaker.

type BreakerConfigurer interface {
	breakerConfigure(g *breakerGuard)
}

type BulkheadConfigurer

BulkheadConfigurer configures a Bulkhead.

type BulkheadConfigurer interface {
	bulkheadConfigure(o *bulkheadGuard)
}

type CLI

CLI is a short command-line command of the product: it runs once, in the CLI profile, and exits with its status. Main runs it when the process's first argument is its name.

type CLI struct{}

type CLIFunc

CLIFunc is a CLI command's body: its arguments after its name, and the streams; it returns the process exit status (0 success, 64 usage, 70 software — the sysexits a shell script branches on).

type CLIFunc func(ctx context.Context, args []string, std StdioValue) int

type ChangeEvent

ChangeEvent describes one transition, for WorkflowService.OnTransition hooks: the entity, the event, and the states it left and entered.

type ChangeEvent[E any, S comparable] struct {
	// Key is the entity's key in the store.
	Key     string
	// Entity is the entity, as stored after the transition.
	Entity  E
	// Event is the transition's name; "create" for [WorkflowService.Start].
	Event   string
	// From is the state left; the zero S on creation.
	From    S
	// To is the state entered.
	To      S
	// Trigger is "create", "event", "timer" or "guard".
	Trigger string
	// Caller is the node that fired it, when code did.
	Caller  string
	// At is when it happened.
	At      time.Time
}

type Classifier

Classifier is an Engine that reads its driver's errors: Transient says whether a failed transaction is one the database asks to run again — on PostgreSQL SQLSTATE 40001 and 40P01, on MySQL errors 1213 and 1205, on SQLite a busy or locked database —, a sibling of Engine. The framework runs such a transaction again, the outermost level only, up to <name>-tx-retries times, waiting <name>-tx-retry-delay grown by 2 with full jitter on the app's clock, each retry a retries attribute on the transaction's span and a retry on the telemetry port. A transaction a Service.Go body opens runs once: the framework cannot know the body may run its function twice. An engine that does not implement it still has the SDK's own reading of SQLSTATE and SQLite result codes.

type Classifier interface {
	Transient(err error) bool
}

func (Classifier) Transient
func (Classifier) Transient(err error) bool

type Command

Command changes something: a typed input, a typed result, and one handler — the one declared with it. Declare it with Service.Command; dispatch it with Command.Dispatch from any building block, which draws a dispatches edge from the caller; give it a route with Command.Expose. A command is internal until it is exposed: how one service has another change something.

A dispatch runs, in this order: the caller's user when the command asks for one (Auth, implied by Command.Allow) — an in-process dispatch carries its caller's and is not authenticated again —, the input's validate tags, the policies (RateLimit, RateLimitPerClient, Timeout, Bulkhead), the key (Command.Key), the transaction, the authorization (Command.Allow, then Command.Authorize), then the handler. A queued command (Queued) is checked at its dispatch, as its caller — its validate tags, its authorization, its key —, and handled by its queue's consumer, the policies around the handling, its key then its transaction around the handler.

A command is its unit of work (ADR 0004): its authorization and its handler run in one transaction (Transact), opened once its key is held, which holds its effects — a publish, a mail, a queued command's dispatch — until the commit. A failed command changes nothing and announces nothing; one dispatched inside another runs in its transaction. NoTransaction opts a command out.

type Command[C, R any] struct{}

func (Command) Allow
func (*Command) Allow(policy authz.Policy, action string, resource string) *Command[C, R]

Allow makes the command need a permission, as data: the app's policy must let the caller — its user, with the attributes its auth data gives when it implements AttrsProvider — do action to resource. The SDK's authz decides, and closes toward refusal: a role with no grant abstains, and an abstention is refused. Allow implies Auth; given twice, both permissions are needed. The refusal is 403 permission_denied, one sentence whatever the cause.

var PlaceOrder = Service.Command("place-order", placeOrder).Allow(Policy, "place", "order")

func (Command) Authorize
func (*Command) Authorize(fn func(context.Context, C) error) *Command[C, R]

Authorize gives the command a rule that needs the data — the ownership of what the input names, the membership of an organisation: fn sees the input and, through its context, the caller (UserID, AuthData), and returns nil, Forbidden, NotFound — to hide what exists — or an authz.Check of its own. It runs last before the handler, after Command.Allow, under the command's key; its refusal is 403 permission_denied, one sentence whatever the cause, and a NotFound or an Unauthenticated stays what it is. The static analysis reads fn as the command's code: the store it reads is the command's edge.

func (Command) Dispatch
func (*Command) Dispatch(ctx context.Context, in C) (R, error)

Dispatch runs the command for the node ctx runs inside — in a span on the command, its caller's dispatches edge —, through its whole pipeline, and returns its result; a queued command returns once its queue accepted it. The caller's user goes with it.

func (Command) DispatchAt
func (*Command) DispatchAt(ctx context.Context, in C, at time.Time) error

DispatchAt queues a dispatch of the queued command that its consumer handles no sooner than at — a reminder, a trial that ends, an order cancelled if still unpaid —, checked now as Command.Dispatch checks it, as its caller: its validate tags, its authorization, its key. It returns once the command's queue accepted it; inside a transaction it is held to the commit as a dispatch is. A dispatch whose key (Command.Key) waits — for later too — or runs is not queued again. An instant already past is now. A command that is not queued refuses it: only a queue holds a dispatch until its instant. It is the SDK's queue.PublishAt.

func (Command) Expose
func (*Command) Expose(route string, opts ...ExposeConfigurer) *Command[C, R]

Expose declares an endpoint that dispatches the command, on route — "METHOD /path/{param}" — with the options an endpoint takes (RateLimitPerClient, MaxBody, Name). It is an endpoint, named after the command — Name for a second exposure —, its request decoded as strictly as any, authenticated when the command asks for a user; the rest — validation, key, authorization — is the command's, so an exposed dispatch and an in-process one are checked alike. It answers 200 with the result, 204 for EmptyValue, 202 for a queued command.

An exposed command says who may run it: a permission (Command.Allow), a rule (Command.Authorize), or, when it declares neither, a marker that it is open on purpose — Anyone, or AnyUser for any signed-in user. The start refuses an exposure that says none of them. A command nobody exposes is internal: the code that needs it dispatches it.

func (Command) Key
func (*Command) Key(fn func(C) string) *Command[C, R]

Key names the entity the command is about: two runs of the command with one key never overlap — the second waits, within its context's deadline —, runs with different keys go at once, and a queued command whose key waits or runs is not queued again until it is handled or dead-lettered. A handler that dispatches its own command with its own key is refused (CodeCommandReentrant), never a deadlock. An input whose key is empty is refused. The key is the span's instance: the Studio filters the runs of one entity.

It is the SDK's lock, in the process: across a restart or between processes, one key can run twice — a handler stays idempotent.

type CommandConfigurer

CommandConfigurer configures a command: Queued; the options every operation takes — Auth, AuthOptional, RateLimit, RateLimitPerClient, Timeout, Bulkhead; and, for a queued one, MaxDeliveries and Parallelism.

type CommandConfigurer interface {
	commandConfigure(o *commandOptions)
}

type CommandLineConfigurer

CommandLineConfigurer tunes a CLI command.

type CommandLineConfigurer interface {
	commandLineConfigure(o *cliOptions)
}

type DatabaseConfigurer

DatabaseConfigurer configures a database: Keeps, Migrations.

type DatabaseConfigurer interface {
	databaseConfigure(o *databaseOptions)
}

type DatabaseURLValue

DatabaseURLValue is what a database's URL points at, as its engine describes it for the graph, the Studio, the logs and every error: never a user, never a password, never a parameter.

type DatabaseURLValue struct {
	// Driver is the Go driver the engine speaks through: "pgx".
	Driver    string
	// Address is where the server listens, host:port; a local socket's
	// path; empty for a file.
	Address   string
	// Database is the database on the server, or the file.
	Database  string
	// TLS is the TLS mode as the URL writes it — "verify-full", "true" —,
	// empty when the URL leaves it to the driver.
	TLS       string
	// Plaintext says the mode the URL writes turns TLS off — sslmode=disable,
	// tls=false —: accepted, for a private network, and drawn "no TLS".
	Plaintext bool
	// Networked says the connection crosses a network, over TCP: outside
	// dev, its URL must write its TLS mode.
	Networked bool
}

type Delay

Delay is how long a timer transition waits: a duration, or a setting that holds one — read when the delay is needed, so each environment can give its own.

type Delay interface {
	time.Duration | *SettingService[time.Duration]
}

type DeliveryOption

DeliveryOption configures a queue's deliveries: a subscription's, or a queued command's (MaxDeliveries, Parallelism).

type DeliveryOption interface {
	SubscriptionConfigurer
	CommandConfigurer
}

type DiagnosticsError

DiagnosticsError is returned by Start when declarations are wrong. It lists every problem at once, each with its position.

type DiagnosticsError struct {
	Diagnostics []model.Diagnostic
	// Missing are the registration packages the program does not import
	// and a declaration needs (ADR 0179 §6), sorted; nil when none is.
	Missing     []string
	// Renamed are the old runtime names the start met (ADR 0180 §3, Q12),
	// each written "<old> -> <new>" — "KIT_DATA_DIR -> TODO_DATA_DIR",
	// `shop.Report.Email kit:"personal" -> app:"personal"` —, sorted; nil
	// when none was.
	Renamed     []string
}

func (DiagnosticsError) Error
func (*DiagnosticsError) Error() string

Error lists every problem the start found, one per line.

func (DiagnosticsError) Unwrap
func (*DiagnosticsError) Unwrap() error

Unwrap is what the refusal is, when the framework has a code for it: with packages in Missing, an error carrying CodeSubsystemMissing (ADR 0179 §6); with names in Renamed, one carrying CodeRenamed (ADR 0180 §3); both joined when the start found both. errs.HasCode reads either through the refusal; nil when it found neither.

type Dialect

Dialect is the SQL an Engine speaks: DialectPostgres, DialectMySQL or DialectSQLite, the framework's own names (ADR 0179 Q5) — the graph names the engine by it.

type Dialect string

type Edit

Edit is one change between two versions of a record, in RFC 6902's words: Op is add, remove or replace, Path the RFC 6901 JSON pointer of the member it changes, From the value it had and To the value it has, as JSON. A secret member says it changed, never what: its From and To are empty.

type Edit = model.Edit

Edit is an alias of github.com/kitsunium/sdk/framework/model/internal/core.EditMessage, declared as:

type Edit struct {
	Op   string          `json:"op"`
	Path string          `json:"path"`
	From json.RawMessage `json:"from,omitempty" app:"personal"`
	To   json.RawMessage `json:"to,omitempty" app:"personal"`
}

type EmptyValue

EmptyValue is the request or the response of an endpoint that has none. An endpoint answering EmptyValue replies 204 No Content.

type EmptyValue struct{}

type EndpointConfigurer

EndpointConfigurer configures an endpoint; each is an ExposeConfigurer too.

type EndpointConfigurer interface {
	endpointConfigure(o *endpointOptions)
	ExposeConfigurer
}

type EndpointService

EndpointService is an HTTP endpoint with a typed request and a typed response: what HTTP itself is about — a webhook a third party calls, say —, the exposure of a command or a query (Command.Expose, Query.Expose), or the implementation of a port (Service.Implement). A business operation is a command or a query, which other code runs with Command.Dispatch and Query.Ask and HTTP reaches through its exposure.

type EndpointService[Req, Resp any] struct{}

func (EndpointService) Call
func (*EndpointService) Call(ctx context.Context, req Req) (Resp, error)

Call runs the endpoint in-process, from another building block: the same validation, the same policies, the same handler, and a calls edge in the diagram from the caller to this endpoint. A port calls its implementation so. Services run each other's operations as commands and queries (Command.Dispatch, Query.Ask); calling a handler function directly works, but the product cannot see it.

type Engine

Engine is an SQL engine a database runs on. An engine module returns one — github.com/kitsunium/sdk/framework/connectors/postgres, …/mysql, …/sqlite —; the framework calls it when the database starts. Like an SDK port, it grows by sibling interfaces, never by a method.

type Engine interface {
	// Describe says what url points at — the driver, host and port, the
	// database, the TLS mode as written — for the graph and the logs: never
	// a user, never a password.
	Describe(url secret.Value) (DatabaseURLValue, error)
	// Dialect is the SQL it speaks.
	Dialect() Dialect
	// Open opens the pool url names. Before each new connection it asks
	// current for the URL as it is now.
	Open(url secret.Value, current func(context.Context) (secret.Value, error)) (*stdsql.DB, error)
}

func (Engine) Describe
func (Engine) Describe(url secret.Value) (DatabaseURLValue, error)

Describe says what url points at — the driver, host and port, the database, the TLS mode as written — for the graph and the logs: never a user, never a password.

func (Engine) Dialect
func (Engine) Dialect() Dialect

Dialect is the SQL it speaks.

func (Engine) Open
func (Engine) Open(url secret.Value, current func(context.Context) (secret.Value, error)) (*stdsql.DB, error)

Open opens the pool url names. Before each new connection it asks current for the URL as it is now.

type Erasure

Erasure is what app.Erase did, store by store.

type Erasure = model.Erasure

Erasure is an alias of github.com/kitsunium/sdk/framework/model/internal/core.ErasureMessage, declared as:

type Erasure struct {
	// Stores are the stores that kept records about the person.
	Stores []StoreErasureMessage `json:"stores"`
}

type Error

Error is an error a product returns to its callers. Message travels on the wire; the cause attached with Error.Wrap is logged and never sent.

Return one from a handler to choose the HTTP status and the message the caller reads. Any other error becomes a 500 whose body says nothing about it: an error message is the classic place a secret leaks from.

type Error struct {
	// Status is the HTTP status.
	Status     int
	// Code is a stable, snake_case identifier a client can branch on.
	Code       string
	// Message is wire-safe text for the caller.
	Message    string
	// Violations detail an invalid request, one per failed rule.
	Violations []ViolationMessage
}

func (Error) Error
func (*Error) Error() string

Error renders the code and the message. It never renders the cause.

func (Error) Unwrap
func (*Error) Unwrap() error

Unwrap returns the cause, so errors.Is and errors.As see through an Error.

func (Error) Wrap
func (*Error) Wrap(cause error) *Error

Wrap attaches the underlying cause, for the logs. It returns a copy.

type ExposeConfigurer

ExposeConfigurer configures an exposure (Command.Expose, Query.Expose): every EndpointConfigurer — Name, MaxBody, RateLimitPerClient… — and the markers that say who may run an operation declaring no rule of its own: Anyone, AnyUser.

type ExposeConfigurer interface {
	exposeConfigure(o *endpointOptions)
}

type FieldRefValue

FieldRefValue is one field of one store that carries a mark: where the field is, and what its tag says of it.

type FieldRefValue struct {
	// Store is the store's node ID.
	Store   string `json:"store"`
	// Path is the field's JSON pointer in the entity: "/body". A "*"
	// stands for every element of an array or every value of a map:
	// "/replies/*/body".
	Path    string `json:"path"`
	// Class is the field's class; "" when it is unclassified.
	Class   string `json:"class,omitempty"`
	// Subject is the pointer of the store's subject — for moderated
	// content, its author — or "" when the store has none.
	Subject string `json:"subject,omitempty"`
}

type Former

Former is one former value of a field that keeps its history: what it was, until when, and who replaced it.

type Former = model.Former

Former is an alias of github.com/kitsunium/sdk/framework/model/internal/core.FormerMessage, declared as:

type Former struct {
	// Value is the field's value then, as JSON; absent for a secret field,
	// whose former values are never given.
	Value json.RawMessage `json:"value,omitempty" app:"personal"`
	// Until is when it was replaced, on the app's clock.
	Until time.Time       `json:"until"`
	// By is the user who replaced it — app.UserID —, empty for a write
	// with no user: a job's, a loop's, kit's own.
	By    string          `json:"by,omitempty"`
}

type Frontend

Frontend is static assets the product serves: a single page application, a documentation site. Requests the page makes to the product's endpoints are attributed to it in the diagram.

type Frontend struct{}

type Job

Job is scheduled work: a piece of the daemon's internal loop. It runs on the SDK scheduler, whose decisions about time are documented rather than emergent: a fire that is due while the previous run of the same job is still going is skipped and counted, and a missed deadline is skipped and counted, never caught up.

type Job struct{}

type JobConfigurer

JobConfigurer configures a job (Service.Every, Service.Cron): Overlap, StartingDeadline.

type JobConfigurer interface {
	jobConfigure(k *jobKnobs)
}

type Keeper

Keeper is what a database keeps: a service — every store it declares —, a store, or a module (ADR 0008) — every store of its services.

type Keeper interface {
	keep() kept
}

type ListenHandler

ListenHandler serves one connection of a Listener until it returns; the connection is closed after it. conn.Peer says who connected, as the kernel says where it can (pkg/v1/proc/ipc).

type ListenHandler func(ctx context.Context, conn *ipc.Conn) error

type Listener

Listener is an inbound port that is not HTTP: a private socket on this machine — a Unix socket in a directory only the product's account can reach, the kernel naming the peer on Linux (ADR 0148) — speaking a versioned contract. It is how a daemon profile serves its clients, and how two process roles of one binary talk (D22): through the contract, never through each other's code.

type Listener struct{}

func (Listener) Dial
func (*Listener) Dial(ctx context.Context) (*ipc.Conn, error)

Dial connects to the listener as a client of the same account would: the same checks, from the same configuration. A test and a sibling role use it.

func (Listener) DialIn
func (*Listener) DialIn(ctx context.Context, product string) (*ipc.Conn, error)

DialIn connects to the listener as run in product, from another process — a client role of the same binary —: the path SocketPathIn computes, the checks Dial makes.

func (Listener) Path
func (*Listener) Path() string

Path is where the listener listens while the app runs; "" otherwise.

func (Listener) SocketPathIn
func (*Listener) SocketPathIn(product string) (string, error)

SocketPathIn is where the listener's socket lies in product — the binary's name for a role of one, else the app's —: its SocketPath, else <runtime dir of product>/<service>-<name>.sock, with SocketPer's key before the extension. A client of another process role computes the daemon's path with it.

type ListenerConfigurer

ListenerConfigurer tunes a listener.

type ListenerConfigurer interface {
	listenerConfigure(o *listenerOptions)
}

type LockWaiter

LockWaiter is an Engine whose connections wait a bounded time for a lock another connection holds — SQLite's busy timeout —, a sibling of Engine (ADR 0178 C2). The framework opens such an engine's pool with OpenWaiting instead of Open, and gives it a wait under <name>-timeout, so that a lost race on the lock comes back as the engine's refusal before the call's own deadline. An engine that does not implement it keeps its own wait.

type LockWaiter interface {
	// OpenWaiting opens the pool url names, as Open does, its connections
	// waiting wait for a lock when url writes no wait of its own.
	OpenWaiting(url secret.Value, current func(context.Context) (secret.Value, error), wait time.Duration) (*stdsql.DB, error)
}

func (LockWaiter) OpenWaiting
func (LockWaiter) OpenWaiting(url secret.Value, current func(context.Context) (secret.Value, error), wait time.Duration) (*stdsql.DB, error)

OpenWaiting opens the pool url names, as Open does, its connections waiting wait for a lock when url writes no wait of its own.

type Loop

Loop is a declared loop of the daemon: the framework owns the wait, the product owns the work. Its wake sources — a period, a deadline the product computes, a topic — are data, so the diagram draws exactly what wakes it; the framework runs it on its own goroutine, one run at a time, observes every run, and restarts it after a panic.

type Loop struct{}

func (Loop) Nudge
func (*Loop) Nudge()

Nudge wakes the loop now, as if its deadline had come. The run's reason is model.WakeManual; a nudge also cuts short the backoff after a failure. It does nothing while the loop is not running.

type LoopConfigurer

LoopConfigurer configures a declared loop.

type LoopConfigurer interface {
	loopConfigure(o *loopOptions)
}

type Mailer

Mailer is outbound mail: a durable outbox and the transport that empties it. Mailer.Send only queues — it returns once the message is safely in the outbox — and the mailer's own loop hands each message to the transport, retrying a failure and dead-lettering a message after its last attempt.

The transport is the SDK's mail package: SMTP when the mail connector's secret smtp-url is set — <PRODUCT>_SMTP_URL, smtp://user:password@host:587?tls= starttls|implicit|none — and otherwise the capture transport, which keeps every message for the Studio's mailbox and for tests (Mailer.Captured) and delivers nothing. Outside dev, the capture transport is reported as a warning. The URL is read again before every delivery: replacing it where it lives — a new version in the store, a new file behind <PRODUCT>_SMTP_URL_FILE — takes effect at the next mail. The outbox and the transports are the SDK's, which the mail subsystem plugs in (framework/app/mail, ADR 0179 step 6).

type Mailer struct{}

func (Mailer) Captured
func (*Mailer) Captured() []model.MailMessage

Captured returns the messages the capture transport kept, oldest first: what a test reads a verification link from. It is empty with SMTP. A mail appears once the outbox has delivered it — shortly after Send.

func (Mailer) Send
func (*Mailer) Send(ctx context.Context, msg mail.Message) (string, error)

Send validates msg with the SDK's mail rules — a header carrying a line break, an unusable address, an empty body are refused here, not at delivery — and puts it in the outbox. It returns the message's outbox ID. Inside a transaction (Transact, a command's) the mail waits for the commit — a rollback drops it —, and the ID returned at once is the one the mail keeps. When the outbox lives on the transaction's database — the one that keeps the mailer's service, by a store's rules — the mail is written in the transaction, sealed, just before it commits: it exists if and only if the transaction commits, the mailbox hears of it after the commit, and an outbox that refuses it fails the transaction. Elsewhere it is put in the outbox after the commit.

A message without a sender gets the mailer's (From); without a date, the app's time; without a Message-ID, one made of its outbox ID, which every attempt keeps, so a receiver can tell a retried delivery from a new mail.

type MailerConfigurer

MailerConfigurer configures a mailer.

type MailerConfigurer interface {
	mailerConfigure(o *mailerOptions)
}

type Mark

Mark is what a module looks for in the product's fields: app.Personal, app.Special or app.Moderated. There is no mark for public or secret: there is nothing to find in the first, and nothing may watch the second.

type Mark string

type MigrationExecutor

MigrationExecutor is what a migration's step runs its statements on: a transaction, which a step cannot end.

type MigrationExecutor interface {
	// ExecContext runs a statement that returns no rows.
	ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)
	// QueryContext runs a query that returns rows; the caller closes them.
	QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)
	// QueryRowContext runs a query that returns at most one row.
	QueryRowContext(ctx context.Context, query string, args ...any) *stdsql.Row
}

func (MigrationExecutor) ExecContext
func (MigrationExecutor) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)

ExecContext runs a statement that returns no rows.

func (MigrationExecutor) QueryContext
func (MigrationExecutor) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)

QueryContext runs a query that returns rows; the caller closes them.

func (MigrationExecutor) QueryRowContext
func (MigrationExecutor) QueryRowContext(ctx context.Context, query string, args ...any) *stdsql.Row

QueryRowContext runs a query that returns at most one row.

type MigrationStep

MigrationStep is one direction of a migration, run on ex — the transaction its version row is written in, so that both commit together.

type MigrationStep func(ctx context.Context, ex MigrationExecutor) error

type MigrationValue

MigrationValue is one versioned change of a database's schema and its reversal, the framework's own (ADR 0179 step 10): the shape the SDK's sql runs, which the store subsystem hands it.

app.Migration{
	Version: 20261017120000,
	Name:    "notes",
	Up:      app.Statements("CREATE TABLE notes (id TEXT PRIMARY KEY, body TEXT)"),
	Down:    app.Statements("DROP TABLE notes"),
}
type MigrationValue struct {
	// Version orders the migration in its set: non-zero and unique; a UTC
	// timestamp is the convention, so that two branches do not collide.
	Version uint64
	// Name labels it in the version table; required.
	Name    string
	// Up applies the change; required.
	Up      MigrationStep
	// Down reverses it; required — [Irreversible] says out loud that it
	// cannot be reversed.
	Down    MigrationStep
}

type Module

Module is a set of services a product mounts as one: NewModule declares it, App.With or Mount mounts it.

type Module struct{}

func (Module) Name
func (*Module) Name() string

Name returns the module's name.

func (Module) Prefix
func (*Module) Prefix() string

Prefix is where the app running the module serves its routes — "/<name>/" unless the mount says otherwise — and its default before an app runs it. A module's user interface, built with relative URLs, finds its API there.

type ModuleConfigurer

ModuleConfigurer is what a module is made of: its services, the modules it requires (Requires), and the migrations of the data it keeps outside the framework's stores (Migrations).

type ModuleConfigurer interface {
	moduleConfigure(m *Module)
}

type MountConfigurer

MountConfigurer configures the mount of a module: Prefix, Bind, Only.

type MountConfigurer interface {
	mountConfigure(d *mountDecl)
}

type OTLPBatchValue

OTLPBatchValue is one batch as the product's own sender receives it (OTLPSender): its signal, OTLPTraces or OTLPMetrics, and its OTLP/JSON document, which a collector takes as it is.

type OTLPBatchValue struct {
	// Signal is OTLPTraces or OTLPMetrics.
	Signal   string
	// Document is the OTLP/JSON export request.
	Document []byte
}

type OTLPFullPolicy

OTLPFullPolicy is what an export's full queue does with a new batch.

type OTLPFullPolicy string

type OpenTelemetryConfigurer

OpenTelemetryConfigurer configures the app's OTLP export: OTLPEndpoint, OTLPBounds, OTLPWhenFull, OTLPMaxAge, OTLPSendTimeout, OTLPSampleRatio, OTLPDatabase, OTLPSender.

type OpenTelemetryConfigurer interface {
	openTelemetryConfigure(o *otlpOptions)
}

type Operation

Operation takes a typed request and answers a typed response: an endpoint, a port — and, with ADR 0005, a command or a query. It is what Fallback and Bind give a port, and what Replace replaces in a test; a request or a response of other types does not compile. Its methods are unexported: only the framework implements it.

Every operation is a node of the graph, and runs in a span of its own through its whole pipeline — authentication, validation, policies, the Studio's mocks — whoever calls it.

type Operation[Req, Resp any] interface {
	edgeKind() model.EdgeKind
	perform(ctx context.Context, req Req) (Resp, error)
	node
}

type OperationOption

OperationOption configures an endpoint, a command or a query alike: the authentication it asks for (Auth, AuthOptional) and the policies in front of its handler (RateLimit, RateLimitPerClient, Timeout, Bulkhead).

type OperationOption interface {
	EndpointConfigurer
	CommandConfigurer
	QueryConfigurer
}

type OverlapPolicy

OverlapPolicy is what a job's fire does while the job's previous run is still going: OverlapSkip, OverlapAllow or OverlapReplace. It is the word of the job's setting <service>-job-<name>-overlap.

type OverlapPolicy string

type PasswordConfigurer

PasswordConfigurer configures a password policy: MinLength, NotReused, NotCommon.

type PasswordConfigurer interface {
	passwordConfigure(o *passwordOptions)
}

type PasswordPolicyService

PasswordPolicyService is the policy of one secret field that holds a password's hash: how long a password is at least, and how many former passwords it refuses. StoreService.Passwords declares it; Set, Change and Verify apply it.

type PasswordPolicyService[T any] struct{}

func (PasswordPolicyService) Change
func (*PasswordPolicyService) Change(ctx context.Context, key string, current []byte, next []byte) error

Change changes the password of the record under key: it verifies the current password first, as Symfony's UserPassword does — a wrong one is an Invalid error —, then sets next as Set does.

func (PasswordPolicyService) Set
func (*PasswordPolicyService) Set(ctx context.Context, key string, password []byte) error

Set sets the password of the record under key — a reset flow, behind a reset token: it checks the policy — the length, the most common passwords, and with NotReused the former passwords —, hashes the password with the SDK's password.Hash and stores the hash; the hash it replaces goes to the field's history. A password the policy refuses is an Invalid error, which says it was used recently, never which one; a missing record is a NotFound.

func (PasswordPolicyService) Verify
func (*PasswordPolicyService) Verify(ctx context.Context, key string, password []byte) (bool, error)

Verify reports whether password is the one of the record under key, for a login, with the SDK's constant-time Verify. When the stored hash is weaker than the SDK's policy now (password.NeedsRehash), the framework stores it again, hashed anew: the same password, so no former value. A key with no record — or a record with no password yet — costs what a wrong password costs, and answers false.

type PersonalData

PersonalData is a person's data as app.Export gives it.

type PersonalData = model.PersonalData

PersonalData is an alias of github.com/kitsunium/sdk/framework/model/internal/core.PersonalDataMessage, declared as:

type PersonalData struct {
	// Stores are the stores that keep records about the person.
	Stores []StoreDataMessage `json:"stores"`
}

type PortConfigurer

PortConfigurer configures a port: Fallback.

type PortConfigurer[Req, Resp any] interface {
	portConfigure(p *PortService[Req, Resp])
}

type PortPolicyConfigurer

PortPolicyConfigurer guards the calls a port makes: Retry, Budget, Breaker, Hedge, Idempotent, Throttle and Timeout, given to PortService.Guard.

type PortPolicyConfigurer interface {
	portPolicyConfigure(g *portGuards)
}

type PortService

PortService is an operation a service needs and does not implement: a typed request, a typed response, a name. Declare it with Service.Port, and call it with PortService.Call as an endpoint is called: validated, observed, drawn.

What a port calls is decided when the app starts, by one rule the static analysis shares: the app's Bind, else the one Service.Implement among the services the app mounts, else the port's own Fallback. The start refuses a port it cannot bind, with every other problem at once.

A port is how a service reaches what it cannot import — a module its host's content, a service another that imports it — the consumer owning the contract.

type PortService[Req, Resp any] struct{}

func (PortService) Call
func (*PortService) Call(ctx context.Context, req Req) (Resp, error)

Call runs what the port is bound to: in a span on the port — the caller's calls edge — then, through the policies PortService.Guard put around it, in the bound operation's own span — one per attempt —, through its whole pipeline, carrying the caller's user as EndpointService.Call does.

func (PortService) Guard
func (*PortService) Guard(opts ...PortPolicyConfigurer) *PortService[Req, Resp]

Guard puts policies around the calls the port makes — Retry, Budget, Breaker, Hedge with Idempotent, Throttle, Timeout — and returns the port. Whatever the order they are given in, they compose the outermost first as Retry, Hedge, Breaker, Throttle, Timeout, around the operation the port is bound to; the retry and the hedge share the port's retry budget.

Each declares, in the port's service, the settings that tune it, named <service>-port-<name>-<knob>: the code's value is the default, which config/<env>.* and <PRODUCT>_<NAME> override, and a value a policy cannot run with stops the start. A port is guarded once: a second Guard is refused.

var Quote = Service.Port[QuoteRequest, Price]("quote").
	Guard(app.Timeout(2*time.Second), app.Retry(3, 100*time.Millisecond), app.Breaker(5, 30*time.Second))

type Query

Query reads and changes nothing: a typed input, a typed result, and one handler — the one declared with it. Declare it with Service.Query; ask it with Query.Ask from any building block, which draws an asks edge from the caller; give it a route with Query.Expose. A query is internal until it is exposed: how one service reads what another keeps. The static analysis warns of a query whose code writes a store, fires a workflow, publishes, sends a mail or dispatches a command.

A question runs, in this order: the caller's user when the query asks for one (Auth, implied by Query.Allow), the input's validate tags, the policies, the authorization (Query.Allow, then Query.Authorize), then the handler, on the caller's goroutine.

type Query[Q, R any] struct{}

func (Query) Allow
func (*Query) Allow(policy authz.Policy, action string, resource string) *Query[Q, R]

Allow makes the query need a permission, as data: the app's policy must let the caller — its user, with the attributes its auth data gives when it implements AttrsProvider — do action to resource. It implies Auth; given twice, both permissions are needed. The refusal is 403 permission_denied, one sentence whatever the cause.

func (Query) Ask
func (*Query) Ask(ctx context.Context, in Q) (R, error)

Ask runs the query for the node ctx runs inside — in a span on the query, its caller's asks edge —, through its whole pipeline, and returns its result. The caller's user goes with it.

func (Query) Authorize
func (*Query) Authorize(fn func(context.Context, Q) error) *Query[Q, R]

Authorize gives the query a rule that needs the data: fn sees the input and, through its context, the caller, and returns nil, Forbidden, NotFound — to hide what exists — or an authz.Check of its own. It runs last before the handler, after Query.Allow; its refusal is 403 permission_denied, one sentence whatever the cause, and a NotFound or an Unauthenticated stays what it is.

func (Query) Expose
func (*Query) Expose(route string, opts ...ExposeConfigurer) *Query[Q, R]

Expose declares an endpoint that asks the query, on route, with the options an endpoint takes (RateLimitPerClient, MaxBody, Name): named after the query, authenticated when the query asks for a user, the rest of the pipeline the query's. It answers 200 with the result, 204 for EmptyValue.

An exposed query says who may ask it: a permission (Query.Allow), a rule (Query.Authorize), or, when it declares neither, a marker that it is open on purpose — Anyone, or AnyUser for any signed-in user. The start refuses an exposure that says none of them. A query nobody exposes is internal: the code that needs it asks it.

type QueryConfigurer

QueryConfigurer configures a query: the options every operation takes — Auth, AuthOptional, RateLimit, RateLimitPerClient, Timeout, Bulkhead — and a transaction's — ReadOnly, Serializable, RepeatableRead —, which run its Ask in a transaction opened with them.

type QueryConfigurer interface {
	queryConfigure(o *queryOptions)
}

type RateLimitConfigurer

RateLimitConfigurer configures a RateLimit.

type RateLimitConfigurer interface {
	rateLimitConfigure(o *rateLimitGuard)
}

type RecordsService

RecordsService is one store's records without their type. Get returns a record without its secret members; Keys lists the records' keys, to a module that watches the store; EraseFields and Delete take a reason and are refused on a held record. Each call is a span on the store's node and an entry in the privacy journal.

type RecordsService struct{}

func (RecordsService) Delete
func (RecordsService) Delete(ctx context.Context, key string, reason string) error

Delete removes the record under key; a held record is a Conflict.

func (RecordsService) EraseFields
func (RecordsService) EraseFields(ctx context.Context, key string, reason string, paths ...string) error

EraseFields clears the fields at paths — pointers as FieldRefValue gives them — in the record under key: the final removal of a moderated field. A path that names no classified field is an Invalid error; a held record is a Conflict.

func (RecordsService) Get
func (RecordsService) Get(ctx context.Context, key string) (json.RawMessage, error)

Get returns the record under key as JSON, its secret members left out.

func (RecordsService) Keys
func (RecordsService) Keys(ctx context.Context) iter.Seq2[string, error]

Keys yields the key of every record the store holds, in key order, and nothing of a record: what a watch reads to catch up on the writes it may have missed. It is offered only to a module one of whose watches the store feeds — any other caller, code outside a module's node included, is refused with a Forbidden error (CodeRecordsKeys) —, and each listing is one entry in the privacy journal, however many keys it yields. A record in the store's trash is not listed. A failure is yielded once, with an empty key, and ends the listing.

type RetryConfigurer

RetryConfigurer configures a Retry.

type RetryConfigurer interface {
	retryConfigure(g *retryGuard)
}

type RevisionEvent

RevisionEvent is one version of a record a store with Revisions keeps: its number — from 1, never given twice while the record exists —, when the write that made it ran, on the app's clock, zero for a record stored before its store kept revisions; who made it — the caller's UserID, empty for a write with no user —; which command made it — its node ID, empty outside a command —; and the record as it was, its secret members zeroed.

type RevisionEvent[T any] struct {
	Number  uint64
	At      time.Time
	By      string
	Command string
	Value   T
}

type Routine

Routine is a loop written by hand: a function that runs until its context ends. The framework starts it with the app, cancels its context on shutdown and waits for it, restarts it after an error or a panic — backing off from one second to one minute — and reads its select statements to draw what it waits on.

type Routine struct{}

type RoutineConfigurer

RoutineConfigurer configures a hand-written loop (Service.Go).

type RoutineConfigurer interface {
	routineConfigure(o *routineOptions)
}

type ScopeValue

ScopeValue is one part of a path computed at the start: the user, the executable, a configuration directory. Two processes share a singleton or a socket when every scope gives them the same value.

type ScopeValue struct{}

type Secret

Secret is a secret the product declares: provided by the operator — an SMTP URL, an API token — or generated by the framework — a signing or sealing key — and rotated on its schedule.

type Secret struct{}

func (Secret) Open
func (*Secret) Open(ctx context.Context, box []byte, aad []byte) ([]byte, error)

Open decrypts a box any kept version sealed, with the aad it was sealed with. A box no kept version opens — forged, altered, or sealed by a version pruned since — is refused with an error errors.Is matches with secret.SealInvalid.

func (Secret) Present
func (*Secret) Present(ctx context.Context) (bool, error)

Present reports whether the secret is set now, where it lives: false, and no error, when an Optional secret is given nowhere — which the product answers by leaving off what needs it. An error says its store could not be read. A secret the start required is present, unless the operator took it back since.

if ok, err := OpenAIKey.Present(ctx); err != nil || !ok {
	return Verdict{}, err // no key, no automatic screening
}

func (Secret) Seal
func (*Secret) Seal(ctx context.Context, plaintext []byte, aad []byte) ([]byte, error)

Seal encrypts plaintext under the secret's current version, binding aad: data that travels beside the box in the clear and must come back unchanged for the box to open.

func (Secret) Sign
func (*Secret) Sign(ctx context.Context, message []byte) ([]byte, error)

Sign signs message with the secret's current version.

func (Secret) Value
func (*Secret) Value(ctx context.Context) (secret.Value, error)

Value returns the secret's current version. A provided secret is read where it lives at every call.

func (Secret) Verify
func (*Secret) Verify(ctx context.Context, message []byte, signature []byte) error

Verify reports whether a kept version signed message: nil when one did, and an error errors.Is matches with secret.SignatureInvalid when none did.

type SecretConfigurer

SecretConfigurer configures a secret.

type SecretConfigurer interface {
	secretConfigure(o *secretOptions)
}

type Service

Service is a bounded context of the product: it owns its building blocks — endpoints, stores, topics, subscriptions, workflows, jobs, frontends — and is the unit an App mounts. Declare one per package, as a package-level variable, and declare its building blocks through its methods:

var Service = app.NewService("todos", "The todo list.")
var Todos = Service.Store("todos", func(t Todo) string { return t.ID })
var _ = Service.Endpoint("GET /todos", List)

Declaring never fails: a mistake — a malformed route, a duplicate name — is recorded with its source position and reported, all at once, when the App starts. The diagram shows it too.

type Service struct{}

func (Service) Activity
func (*Service) Activity(fn func(context.Context) bool) *ActivityHandler

Activity declares that the service is busy while fn returns true: a daemon whose clients are sessions, not connections, stops once the last session ended rather than once the last connection closed. fn is called on the idle watch's tick, from one goroutine, and must answer quickly; a panic counts as busy, and is logged. The value is kept for the declaration:

var _ = Service.Activity(func(context.Context) bool { return sessions.Open() > 0 })

func (Service) AuthHandler
func (*Service) AuthHandler[P, D any](name string, handler func(context.Context, P) (UID, D, error)) *AuthenticatorHandler[P, D]

AuthHandler declares the app's authentication handler. The handler returns the user's ID and data; an empty UID with a nil error means "no credentials", which Auth endpoints answer 401 and AuthOptional endpoints serve anonymously; an error — usually Unauthenticated — refuses the request.

The handler runs only for a request that carries at least one of the credentials' cookies or headers; a request with none is anonymous without asking it.

func (Service) CLI
func (*Service) CLI(name string, doc string, fn CLIFunc, opts ...CommandLineConfigurer) *CLI

CLI declares a command named name, which Main runs when the process is called "<binary> <name> ...". doc is its one-line help.

func (Service) Command
func (*Service) Command[C, R any](name string, handler func(context.Context, C) (R, error), opts ...CommandConfigurer) *Command[C, R]

Command declares a command of the service: an operation named name, run by handler, that changes something. Its node is "<service>/command/<name>".

var PlaceOrder = Service.Command("place-order", placeOrder).
	Allow(Policy, "place", "order").
	Expose("POST /orders")

func (Service) Cron
func (*Service) Cron(name string, expr string, fn func(context.Context) error, opts ...JobConfigurer) *Job

Cron declares a job that runs on a five-field POSIX cron expression, in the process's local time zone. A fire due while its previous run still goes is skipped, and a late fire runs however late, unless Overlap and StartingDeadline say otherwise.

func (Service) Endpoint
func (*Service) Endpoint[Req, Resp any](route string, handler func(context.Context, Req) (Resp, error), opts ...EndpointConfigurer) *EndpointService[Req, Resp]

Endpoint declares an HTTP endpoint on route — "METHOD /path/{param}" — run by handler. The request is decoded from the path, the query, the headers (fields tagged `path:"…"`, `query:"…"`, `header:"…"`) and the JSON body (every other field), then checked against its `validate` tags, then handed to the policies (RateLimit, Timeout, Bulkhead) and the handler. The response is JSON, or 204 No Content when it is EmptyValue.

An endpoint is for what HTTP itself is about — a webhook a third party calls. A business operation is a command or a query, exposed on its route (Command.Expose, Query.Expose), and run in process by the code that needs it (Command.Dispatch, Query.Ask).

The endpoint's name, and so its node ID, is the handler's function name.

func (Service) Every
func (*Service) Every(name string, period time.Duration, fn func(context.Context) error, opts ...JobConfigurer) *Job

Every declares a job that runs every period, first one period after the app starts. A fire due while its previous run still goes is skipped, and a late fire runs however late, unless Overlap and StartingDeadline say otherwise.

func (Service) Go
func (*Service) Go(name string, run func(context.Context) error, opts ...RoutineConfigurer) *Routine

Go declares a hand-written loop. run must return when ctx ends; returning earlier, with or without an error, restarts it after a backoff — more than ten times within ten minutes, and it is not restarted again: the product is no longer live (GiveUpAfter).

Everything run does is drawn from the loop's node: a store it reads, a topic it publishes on. The framework has no span around it — a run lasts as long as the app — so each of those calls starts a trace of its own.

func (Service) Implement
func (*Service) Implement[Req, Resp any](port *PortService[Req, Resp], handler func(context.Context, Req) (Resp, error), opts ...EndpointConfigurer) *EndpointService[Req, Resp]

Implement declares that the service implements port: an endpoint of the service with no route, which only the port reaches, in process, when the app binds nothing else and this is the one implementation among the services it mounts. It runs as every endpoint does — the request's validate tags, the options' policies, the caller's user — and is named like one: its Name, else its handler's name, else, for a function literal, the port's name. A handler whose request or response is not the port's does not compile. A host implements a module's port on its own service: it never adds a node to the module's.

var _ = posts.Service.Implement(desk.Enforcer, Enforce)

func (Service) Listen
func (*Service) Listen(name string, contract string, handler ListenHandler, opts ...ListenerConfigurer) *Listener

Listen declares a listener named name speaking contract — "<name>/v<major>", "render/v1" — whose connections handler serves, one goroutine each.

func (Service) Loop
func (*Service) Loop(name string, run func(context.Context, WakeEvent) error, opts ...LoopConfigurer) *Loop

Loop declares a loop run by kit: run is called once at start, then on every wake.

Runs never overlap: wakes that come while the function runs are gathered into one run after it. A run that fails or panics is counted, and the next one waits a backoff — one second, doubling up to a minute — unless it is a Loop.Nudge. A deadline already past wakes the loop at once, but never sooner than a second after the previous run: a WakeAt that keeps answering the past costs a run a second, not a spinning core. A loop whose runs failed more than ten times within ten minutes stops, and the product is no longer live (GiveUpAfter).

func (Service) Mailer
func (*Service) Mailer(name string, opts ...MailerConfigurer) *Mailer

Mailer declares outbound mail.

func (Service) Name
func (*Service) Name() string

Name returns the service name, which is also its node ID: qualified with its module's, "moderation.intake", when a module lists it.

func (Service) Port
func (*Service) Port[Req, Resp any](name string, opts ...PortConfigurer[Req, Resp]) *PortService[Req, Resp]

Port declares a port of the service: an operation it needs, named name, taking Req and answering Resp, which another service implements (Service.Implement) or the app binds (Bind). Its node is "<service>/port/<name>"; its doc is its declaration's doc comment.

func (Service) Query
func (*Service) Query[Q, R any](name string, handler func(context.Context, Q) (R, error), opts ...QueryConfigurer) *Query[Q, R]

Query declares a query of the service: an operation named name, run by handler, that reads and changes nothing. Its node is "<service>/query/<name>".

var MyOrders = Service.Query("my-orders", myOrders, app.Auth()).Expose("GET /orders", app.AnyUser())

func (Service) Secret
func (*Service) Secret(name string, opts ...SecretConfigurer) *Secret

Secret declares a secret the product needs.

Without an option it is provided: the operator gives it in the environment — the variable <APP>_<NAME>, TODO_STRIPE_KEY for the secret "stripe-key" of the app "todo", or TODO_STRIPE_KEY_FILE naming a file that holds it, the Docker and Kubernetes way — or in the environment's store, with `the framework secrets set`. It is read at every use, so replacing it where it lives takes effect without a restart, and a start without it fails, naming the variable to set.

With Optional, a start without it goes on: the product asks Secret.Present before it uses the secret.

With Generated, the framework makes it: version 1 on the first start, a new version on its schedule, the previous ones kept so that what they sealed or signed still opens and verifies. It lives in the store the environment keeps — an encrypted file store in dev, <PRODUCT>_SECRETS elsewhere. Its variable pins it: the framework then uses the value as it is and does not rotate it.

name is 1 to 63 lower-case letters, digits and dashes, starting and ending with a letter or a digit — one grammar for a file name and a variable — and unique in the app.

func (Service) Setting
func (*Service) Setting[T SettingValue](name string, def T, opts ...SettingConfigurer) *SettingService[T]

Setting declares a setting of the service: name holds def unless the environment says otherwise — the variable <APP>_<NAME>, TODO_BASE_URL for the setting "base-url" of the app "todo", or the key name in the product's configuration files. SettingService.Get reads it.

name follows the secrets' grammar — 1 to 63 lower-case letters, digits and dashes, starting and ending with a letter or a digit — and is unique in the app, among its settings and its secrets: the two share their variables.

func (Service) Static
func (*Service) Static(name string, prefix string, fsys fs.FS, opts ...StaticConfigurer) *Frontend

Static declares a frontend serving fsys under prefix, which must end with '/'. A path with no file extension that matches no file serves index.html, so client-side routing works; a missing file with an extension — a script, a stylesheet — stays a 404. A directory is never listed. The SDK's static handler serves it; the framework draws it and observes every request.

func (Service) Store
func (*Service) Store[T any](name string, key func(T) string, opts ...StoreConfigurer) *StoreService[T]

Store declares a store of entities of type T, keyed by key.

func (Service) Subscribe
func (*Service) Subscribe[T any](name string, topic *TopicService[T], handler func(context.Context, T) error, opts ...SubscriptionConfigurer) *SubscriptionWorker[T]

Subscribe declares a subscription of the service to topic. Delivery is at least once: handler must be idempotent, which is the one thing the SDK's queue cannot check and the framework therefore asks you to promise here.

func (Service) Topic
func (*Service) Topic[T any](name string) *TopicService[T]

Topic declares a topic carrying messages of type T.

func (Service) Watch
func (*Service) Watch(name string, mark Mark, handler func(context.Context, WrittenEvent) error, opts ...SubscriptionConfigurer) *Watch

Watch declares a watch of the service: handler is told of every write the framework confirms on a store whose entity holds a field that carries mark — the product's stores and the other modules', never those of the module the service belongs to (for a service of the product's, never the product's own) unless OwnStores says so. The notice says which store, which record and whether it was deleted; the handler reads the record itself (RecordsOf). Delivery is at least once and in no set order, retried and dead-lettered after MaxDeliveries attempts: handler must be idempotent, and it hears the writes it makes itself on another's store — with OwnStores, never a write its own handler makes. After a stop that was not clean — the process died with the watch running —, its next start queues a notice for every record of every store that feeds it, so that no write a crash took the notice of goes unheard; a deletion whose notice was lost is not among them. The mark that says a watch runs is kept in the data directory: an app without one does not catch up (ADR 0178 C6).

var Content = Service.Watch("content", app.Moderated, Screen)
var Posts = Service.Watch("posts", app.Moderated, Stamp, app.OwnStores())

func (Service) Workflow
func (*Service) Workflow[E any, S comparable](name string, store *StoreService[E], state func(*E) *S) *WorkflowService[E, S]

Workflow declares a state machine over the entities of store. state returns a pointer to the entity's state field.

type SettingConfigurer

SettingConfigurer configures a setting.

type SettingConfigurer interface {
	settingConfigure(o *settingBase)
}

type SettingService

SettingService is a value the environment gives a service, declared with Service.Setting.

type SettingService[T SettingValue] struct{}

func (SettingService) Get
func (*SettingService) Get() T

Get returns the setting's value in the app the service is mounted in — its default until an app has started with it.

func (SettingService) Name
func (*SettingService) Name() string

Name is the setting's name, its key in a configuration file — in its module's section, for a module's setting.

type SettingValue

SettingValue is what a setting may hold.

type SettingValue interface {
	string | bool | int | int64 | float64 | time.Duration | []string
}

type StaticConfigurer

StaticConfigurer configures a frontend.

type StaticConfigurer interface {
	staticConfigure(o *staticOptions)
}

type StdioValue

StdioValue is where a CLI command reads and writes: the process's own streams under Main, buffers in a test.

type StdioValue struct {
	// In is the command's standard input.
	In          io.Reader
	// Out and Err are its standard output and error.
	Out         io.Writer
	// Out and Err are its standard output and error.
	Err         io.Writer
	// Interactive reports whether In is a terminal.
	Interactive bool
}

type StoreConfigurer

StoreConfigurer configures a store.

type StoreConfigurer interface {
	storeConfigure(o *storeOptions)
}

type StoreService

StoreService is a typed, keyed collection of entities. Every read returns a copy and every write stores one: an entity is kept as its JSON encoding, so what a handler holds can never alias what the store holds, and the memory and file backends behave identically.

With a data directory (<PRODUCT>_DATA_DIR; ".app/data" in dev) the store persists every write before returning, atomically — a crash never leaves a torn file — and a write costs one entity, whatever the store holds. Without one, it lives in memory. A store a database keeps (Database, Keeps) lives in a table of it, and every call is a round trip. A store is a port (ADR 0004): what it runs on is its engine (store_engine.go) — the SDK's document store, in memory, in files or over SQL —; the framework makes it a node of the graph, observes every call, and speaks for its refusals. Its writes join the transaction their context carries (Transact).

type StoreService[T any] struct{}

func (StoreService) Count
func (*StoreService) Count(ctx context.Context) (int, error)

Count returns how many entities the store holds.

func (StoreService) Delete
func (*StoreService) Delete(ctx context.Context, key string) error

Delete removes the entity with the given key, or returns a NotFound error. On a store with Revisions it moves the entity to the store's trash instead, for <store>-trash — 30 days unless that setting says otherwise, 0 deleting at once —: gone from every read, its unique keys free, its versions kept, StoreService.Untrash brings it back, and the framework purges it once its time is up. When the store seals at rest and the entity was its person's last record, their data key goes with it, as with an erasure — at the commit, in a Transact —, once nothing the framework keeps still needs it: a held record, a record in the trash, or a former value or version sealed under it, keeps the key.

func (StoreService) Diff
func (*StoreService) Diff(ctx context.Context, key string, from uint64, to uint64) ([]Edit, error)

Diff returns what changed from the version numbered from to the version numbered to of the record under key, in RFC 6902's words — add, remove, replace, each at a JSON pointer — with both values, in the order they apply: none when the two are the same. A secret member says it changed, never what. Showing a text's changes line by line is the reader's: the Studio does it.

func (StoreService) Erase
func (*StoreService) Erase(ctx context.Context, key string, reason string) error

Erase erases one record now, as its retention would: its personal, special and secret members and its subject cleared — after the store's Anonymise function has kept what it generalises —, its erased fields stamped, the rest kept. A held record is refused with a Conflict that names the hold, never a placed hold's reason. reason is journaled, for whoever runs the product.

func (StoreService) Filter
func (*StoreService) Filter(ctx context.Context, keep func(T) bool) ([]T, error)

Filter returns the entities keep accepts, ordered by key. It reads every entity: an index is the way to avoid it.

func (StoreService) Find
func (*StoreService) Find(ctx context.Context, index string, key string) ([]T, error)

Find returns the entities indexed under key, ordered by their store key. It reads any index, unique or not; no entity is an empty slice, not an error.

func (StoreService) Former
func (*StoreService) Former(ctx context.Context, key string, pointer string) ([]Former, error)

Former returns the former values of the field at pointer — its JSON pointer (RFC 6901), "/email" — in the record under key, newest first: each with when it was replaced and by whom. A secret field's former values say when they changed, never what they were: their Value is empty. A field that keeps no former values is an Invalid error, a missing record a NotFound.

func (StoreService) Get
func (*StoreService) Get(ctx context.Context, key string) (T, error)

Get returns the entity with the given key, or a NotFound error.

func (StoreService) Hold
func (*StoreService) Hold(ctx context.Context, key string, reason string) error

Hold places a legal hold on the record under key: neither the retention, nor a person's erasure, nor Delete touches it until StoreService.Release lifts it. reason says why — an authority's request, a case under litigation — and is kept for whoever runs the product, never shown in the Studio. Holding a held record again replaces its reason.

func (StoreService) Insert
func (*StoreService) Insert(ctx context.Context, v T) error

Insert stores a new entity; it is a Conflict error when the key exists.

func (StoreService) List
func (*StoreService) List(ctx context.Context) ([]T, error)

List returns every entity, ordered by key.

func (StoreService) Lookup
func (*StoreService) Lookup(ctx context.Context, index string, key string) (T, error)

Lookup returns the entity whose key in the unique index is key, or a NotFound error. Asking a non-unique index is an Invalid error: use StoreService.Find.

func (StoreService) Passwords
func (*StoreService) Passwords(field func(*T) *string, opts ...PasswordConfigurer) *PasswordPolicyService[T]

Passwords declares the password policy of one secret field of the store's entity; field names it — the framework calls it on a zero entity to find which. The field then only ever holds the password's hash, a PHC string, or nothing before a first password; a write that puts anything else in it is refused. A field that is not secret refuses the start, as does a second policy on the same field.

func (StoreService) Put
func (*StoreService) Put(ctx context.Context, v T) error

Put stores the entity, replacing any entity with the same key.

func (StoreService) Release
func (*StoreService) Release(ctx context.Context, key string) error

Release lifts the hold on the record under key: the retention takes it up again, from where its instants say. A record no hold keeps is a NotFound error.

func (StoreService) Restore
func (*StoreService) Restore(ctx context.Context, key string, number uint64, fields ...string) (T, error)

Restore writes the version numbered number of the record under key back, as a new version: history is never rewritten. It restores the whole version, or only the fields named by JSON pointer (RFC 6901) — "/title" —, as WordPress restores some fields of a revision. It keeps the record's workflow state, which its workflow owns, and every secret member: restoring an account never brings back an old password past its policy, and naming one of them is an Invalid error. A unique index the restored record breaks refuses it with a Conflict, and nothing changes. It returns the record as restored.

func (StoreService) Revision
func (*StoreService) Revision(ctx context.Context, key string, number uint64) (RevisionEvent[T], error)

Revision returns the version numbered number of the record under key, its secret members zeroed: a NotFound when the record keeps none of that number — never made, or pruned.

func (StoreService) Revisions
func (*StoreService) Revisions(ctx context.Context, key string) ([]RevisionEvent[T], error)

Revisions returns the versions the store keeps of the record under key, newest first: the record as it is now, then the former ones — n at most, more while a legal hold keeps the record. A secret member is zeroed in each: a former password hash never reaches the product. A missing record is a NotFound, a store that keeps no revisions an Invalid.

func (StoreService) Trashed
func (*StoreService) Trashed(ctx context.Context) ([]TombstoneEvent, error)

Trashed returns the records in the store's trash, in key order: each one's key, when the product deleted it, and when the framework purges it. A store without Revisions keeps no trash: an Invalid.

func (StoreService) Untrash
func (*StoreService) Untrash(ctx context.Context, key string) (T, error)

Untrash brings the record the product deleted under key back from the store's trash, as it was when deleted — its versions kept, no version made — and returns it. A key with no record in the trash — never deleted, purged, or written again since — is a NotFound; a unique index another record took a key of meanwhile refuses it with a Conflict, and it stays in the trash; a store without Revisions keeps no trash: an Invalid.

func (StoreService) Update
func (*StoreService) Update(ctx context.Context, key string, fn func(*T) error) (T, error)

Update applies fn to the entity with the given key and stores the result, atomically with respect to every other write of this store. fn must not change the key. An error from fn leaves the entity untouched.

fn runs while the store's writers wait — on a database, inside the write's transaction, the entity locked —: it may read this store, but must not write to it, nor fire a workflow over it — either would wait for the write fn is part of.

type SubscriptionConfigurer

SubscriptionConfigurer configures a subscription.

type SubscriptionConfigurer interface {
	subscriptionConfigure(o *subscriptionOptions)
}

type SubscriptionWorker

SubscriptionWorker consumes a topic: its handler runs once per message, at least once, retried on error, and dead-lettered after MaxDeliveries attempts.

type SubscriptionWorker[T any] struct{}

type SupervisionConfigurer

SupervisionConfigurer configures what the framework restarts after a failure: a subscription's, a watch's or a queued command's consumer, a declared loop, a hand-written loop (GiveUpAfter).

type SupervisionConfigurer interface {
	DeliveryOption
	LoopConfigurer
	RoutineConfigurer
}

type TimeoutConfigurer

TimeoutConfigurer configures an endpoint, a command, a query and a port alike: Timeout.

type TimeoutConfigurer interface {
	OperationOption
	PortPolicyConfigurer
}

type TombstoneEvent

TombstoneEvent is one record in a store's trash: its key, when the product deleted it, on the app's clock, and when the framework purges it — its trash's length after the deletion, or sooner when its retention says so.

type TombstoneEvent struct {
	Key     string
	At      time.Time
	PurgeAt time.Time
}

type TopicService

TopicService is an asynchronous channel of messages of type T. Every subscription receives every message, at least once: a subscription's handler must tolerate a redelivery.

type TopicService[T any] struct{}

func (TopicService) Once
func (*TopicService) Once(key func(T) string, window time.Duration) *TopicService[T]

Once makes the topic publish a message at most once per key over window: a second TopicService.Publish whose message has the key of one published within the window publishes nothing, in no subscription's queue, and answers nil — a producer that retries a publication it never heard back from adds no copy. The window is counted by each subscription's queue, from its first publication under the key; it is the SDK's queue.PublishOnce. A message whose key is empty is refused. Delivery stays at least once: a handler still tolerates a redelivery.

window is the default of the topic's setting <service>-topic-<name>-once-window, which config/<env>.* and <PRODUCT>_<NAME> override; one that is not positive stops the start. A queued command keeps its own key (Command.Key): a dispatch whose key waits or runs is not queued again, however long ago it was dispatched.

func (TopicService) Publish
func (*TopicService) Publish(ctx context.Context, msg T) error

Publish sends msg to every subscription. It returns once every subscription's queue has accepted the message; delivery happens later. Inside a transaction (Transact, a command's) the message is encoded now and queued at the commit: in the transaction, just before it commits, for a subscription whose queue lives on the transaction's database — a queue that refuses it then fails the transaction —, once it committed for the others. A rollback drops it. A topic that publishes once (TopicService.Once) publishes a message whose key it published within its window in no queue.

func (TopicService) PublishAt
func (*TopicService) PublishAt(ctx context.Context, msg T, at time.Time) error

PublishAt sends msg to every subscription as TopicService.Publish does, but no subscription receives it before at: a reminder, a trial that ends, an order cancelled if still unpaid. It is as durable as any message, held to the commit inside a transaction as a publication is; an instant already past is now. It does not recur — a job does. A topic that publishes once refuses it: the SDK has no publication once for later yet. It is the SDK's queue.PublishAt.

type TransactOption

TransactOption asks a transaction for an isolation level or for read-only: Serializable, RepeatableRead, ReadOnly. None given is the database's default, as always.

type TransactOption struct{}

type UID

UID identifies an authenticated user. It is what the auth handler returns and what every endpoint reads with UserID.

type UID string

type ViolationMessage

ViolationMessage is one validation rule a request failed: where, which rule, and why — never the value.

type ViolationMessage struct {
	// Path locates the value: "title", "items[2].zip".
	Path    string `json:"path"`
	// Rule names the failed rule: "required", "maxlen".
	Rule    string `json:"rule"`
	// Message explains it without echoing the value.
	Message string `json:"message"`
}

type WaitConfigurer

WaitConfigurer configures a RateLimit and a Bulkhead alike.

type WaitConfigurer interface {
	RateLimitConfigurer
	BulkheadConfigurer
}

type WakeEvent

WakeEvent says why a declared loop runs: the reason, the instant, and the topic that woke it when one did. Wakes that come while it runs, or while it backs off after a failure, are gathered into one run: Reasons and Topics say every one of them, and Reason the one that decides the run.

type WakeEvent struct {
	// Reason is model.WakeInterval, WakeDeadline, WakeTopic, WakeStart or
	// WakeManual: WakeManual when a nudge is among the wakes the run
	// gathers — the one wake that cuts a backoff short —, else the first
	// that came.
	Reason  string
	// At is when the wake happened, on the app's clock.
	At      time.Time
	// Topic is the topic node whose publish woke the loop first, when a
	// topic's did.
	Topic   string
	// Reasons are the reasons of every wake the run gathers, each once, in
	// the order they came: Reason is among them.
	Reasons []string
	// Topics are the topic nodes whose publish woke the loop into this run,
	// each once, in the order they came: Topic is the first.
	Topics  []string
}

type Watch

Watch is a subscription fed by stores instead of a topic: its handler is told of every write the framework confirms on a store whose entity holds a field with its mark. Service.Watch declares one.

type Watch struct{}

type WorkflowService

WorkflowService is a state machine over the entities of one store: the states an entity goes through, and the arrows between them. It is declared as data — which is why the diagram can draw it exactly — and it runs four kinds of transitions:

The SDK's state-machine engine runs it (statemachine), which the workflow subsystem plugs in (framework/app/workflow, ADR 0179 step 5). Its loop never polls: it keeps an agenda of the transitions due, sleeps until the earliest, and a write of the store — which may bring one forward — wakes it. A workflow with nothing due does not run at all. Transitions of one entity run one at a time, of different entities at once.

The entity's state lives in the entity itself (the state function returns a pointer to it), so the store stays the single source of truth; the engine keeps, beside it, when each entity entered its state and the history of its transitions — in the workflow's file under the data directory, so a timer keeps counting across a restart.

type WorkflowService[E any, S comparable] struct{}

func (WorkflowService) After
func (*WorkflowService) After[D Delay](event string, d D, from S, to S) *WorkflowService[E, S]

After declares a timer transition: an entity that has spent d in state from moves to state to, fired by the workflow's own loop. d is a duration, or a setting of the service that holds one — which must then be positive.

func (WorkflowService) At
func (*WorkflowService) At(event string, from S, to S, instant func(E) (time.Time, bool)) *WorkflowService[E, S]

At declares a timer transition at an instant the entity carries — a due date, a deadline: an entity in state from moves to state to once the time instant returns has come; false means no instant for now. The framework asks again each time the entity is written, and the workflow's own loop sleeps until the earliest instant.

func (WorkflowService) Can
func (*WorkflowService) Can(e E, event string) bool

Can reports whether event may be fired on e.

func (WorkflowService) Census
func (*WorkflowService) Census(ctx context.Context) (map[S]int, error)

Census counts the entities per state; every declared state is present. The engine keeps the count; the call is drawn, as the diagram declares it, as a read of the store.

func (WorkflowService) Fire
func (*WorkflowService) Fire(ctx context.Context, key string, event string) (E, error)

Fire moves the entity with the given key along the event transition leaving its current state. It is a Conflict error when no such transition exists, and a NotFound error when the entity does not.

func (WorkflowService) Initial
func (*WorkflowService) Initial(state S) *WorkflowService[E, S]

Initial sets the state a new entity enters with WorkflowService.Start.

func (WorkflowService) On
func (*WorkflowService) On(event string, from S, to S) *WorkflowService[E, S]

On declares an event transition: WorkflowService.Fire with this event moves an entity in state from to state to. One event may leave several states, but only one arrow per (event, from).

func (WorkflowService) OnEnter
func (*WorkflowService) OnEnter(state S, fn func(context.Context, *E) error) *WorkflowService[E, S]

OnEnter runs fn when an entity enters state, before it is stored. An error cancels the transition, and so does a panic — reported as CodeWorkflowHookPanic, the entity free for the next transition. fn may change the entity, not its state nor its key (CodeWorkflowHookChange), and must not fire this workflow, which is refused (CodeWorkflowReentrant): transitions of one entity run one at a time.

func (WorkflowService) OnTransition
func (*WorkflowService) OnTransition(fn func(context.Context, ChangeEvent[E, S]) error) *WorkflowService[E, S]

OnTransition runs fn after every transition has been stored, whoever fired it — the natural place to publish it on a topic. An error or a panic is logged and shown by the Studio; the transition stands, and the next hooks still run.

func (WorkflowService) Start
func (*WorkflowService) Start(ctx context.Context, e E) (E, error)

Start enters a new entity into the workflow: it sets the initial state, runs the OnEnter hooks, and inserts the entity in the store.

func (WorkflowService) When
func (*WorkflowService) When(event string, from S, to S, guard func(E) bool) *WorkflowService[E, S]

When declares a guard transition: an entity in state from moves to state to as soon as guard holds. The guard sees the entity only: the framework checks it each time the entity is written, never on a clock — a condition that depends on the time is an instant, declared with WorkflowService.At.

type WrittenEvent

WrittenEvent is what a watch is told of a write: which store, which record, and whether it was deleted — never a value. The watch reads the record itself (RecordsOf): without its secret members, and journaled.

type WrittenEvent struct {
	// Store is the store's node ID: "shop/store/reviews".
	Store   string          `json:"store"`
	// Key is the record's key in the store.
	Key     string          `json:"key"`
	// Deleted is set when the write deleted the record: there is nothing
	// left to read.
	Deleted bool            `json:"deleted,omitempty"`
	// Fields are the store's fields that carry the watch's mark, as
	// [App.Fields] lists them. Which of them the write changed, the notice
	// does not say.
	Fields  []FieldRefValue `json:"fields,omitempty"`
}

Generated from docs/api by tools/genindex.

Documentation

Overview

Package app — who may call an operation: the principal, the rules and their checks.

Package app — activities: what a daemon's idle stop waits for besides its connections, a notion of the product's own — sessions still open, work still queued.

Package app — the App: the services a product mounts, served by one process.

Package app — the static analysis a product can be given: the platform's analyzer, run in the background in dev (ADR 0147 §1).

Package app — process profiles: what an app does with its process (ADR 0147 §5) — serve, run as a daemon, or run one CLI command — and the singleton lock.

Package app — the architecture the graph draws: where each store keeps its data.

Package app — authentication: the handler that says who calls, and the user it names.

Package app — the build a graph describes: the modules and versions it was made of.

Package app — the catalog of the generic mechanics a node is built from.

Package app — the app tag: the classes and words a field of personal data carries.

Package app — the plan of a Go type's classified members, computed once per type.

Package app — the walk of a value along its classification plan.

Package app — the warnings for fields whose name suggests personal data left unclassified.

Package app — CLI commands: a short command of the product that Main runs once, in the CLI profile, and whose status is the process's.

Package app — commands: an operation that changes something, with one handler.

Package app — the app's options and environments.

Package app — the config command: every setting, its value and where it comes from.

Package app — the consumer that drains a queue into its handler.

Package app — the daemon: what a running app knows of itself beyond its nodes.

Package app — the daemon's HTTP server as the Studio sees it: its loop, its counters, and the middleware every request goes through.

Package app — the daemon's loops: their states, the scheduler that fires the jobs, and the goroutine labels that name them.

Package app — databases: the SQL engines a store can be kept on.

Package app — the start's judgement of the declared databases.

Package app — the interfaces of databases: the engine a connector gives, and what the framework asks of it.

Package app — the migrations a database runs, and their lock.

Package app — the placement of each store: which database keeps its data.

Package app — a database in a running app.

Package app — the databases as the graph shows them.

Package app — the decoding of a request into its typed value.

Package app — the dev tools' routes of the Studio, read-only.

Package app is the implementation of framework/app, the framework in which every product is its own diagram. A product imports framework/app, whose names alias and forward to this package's: here, the types carry the role their name says — StoreService, EndpointService, AppConfigurer — and the facade keeps the names a product writes — Store, Endpoint, AppOption.

Package app — endpoints: an HTTP route and the operation it serves.

Package app — the errors a product returns to its callers, and their wire mapping.

Package app — exposures: an operation an endpoint serves over HTTP.

Package app — what an exposure takes, and what it exposes.

Package app — the Go modules of the running binary, read from its build info.

Package app — the graph: the product as a diagram of what it mounts.

Package app — history: the former values of the fields that keep them.

Package app — the history's storage: former values kept beside each record.

Package app — reading a record's former values.

Package app — the history as the graph and the Studio show it.

Package app — what the framework's own writes mean to the history.

Package app — legal holds: records kept whatever their retention says.

Package app — secondary indexes of a store.

Package app — the Studio's read API: the graph, the events, the sources.

Package app — what the source endpoint needs of the root it reads.

Package app — how the source endpoint opens a file on Unix.

Package app — how the source endpoint opens a file on Windows: the link itself, never its target.

Package app — jobs: scheduled work run by the daemon's loop.

Package app — the privacy journal: what the framework did to personal data, and why.

Package app — idempotency keys and their leases.

Package app — the compile-time proof of which roles each declaration plays: the interfaces the app reaches its nodes and options through.

Package app — listeners: inbound ports that are not HTTP, a private socket speaking a versioned contract (ADR 0148).

Package app — the compile-time proof that a listener is a starter.

Package app — what a listener needs of the socket it accepts on.

Package app — the product's logger and the logs the Studio shows.

Package app — loops: work the daemon waits for, woken by time or by events.

Package app — mailers: a durable outbox and the transport that empties it.

Package app — what building an SMTP transport needs of its URL.

Package app — Main: the whole main function of a product.

Package app — the migrate command.

Package app — modules: a set of services a product mounts as one.

Package app — the start's checks of the modules an app mounts.

Package app — what a module keeps: its stores and its migrations.

Package app — the mount of a module: its prefix and its bindings.

Package app — nodes: what every declaration shares, and its source position.

Package app — the interfaces of nodes: what every declaration is, and the roles some play.

Package app — observation: the spans and events of a running product.

Package app — operations: a typed request answered by a typed response.

Package app — the password policy of a store's fields.

Package app — hashing and verifying passwords under their policy.

Package app — the warnings for endpoints that set passwords outside a policy.

Package app — ports: an operation a service needs and does not implement.

Package app — privacy: the framework's own service for holds and the journal.

Package app — erasure: a record's personal data removed as its retention would.

Package app — export: a person's data as the framework gives it back.

Package app — a store's personal data as the graph shows it.

Package app — the keys that index personal data without revealing it.

Package app — the Studio's privacy pages: the journal, the holds, the register.

Package app — the privacy command.

Package app — what the privacy command prints.

Package app — `privacy seal`: a whole store sealed now.

Package app — the process sample: what the running process uses.

Package app — the Studio's profiler: the live heap and the goroutines, folded onto the graph.

Package app — queries: an operation that reads and changes nothing.

Package app — queued commands: a command run later, from its queue.

Package app — rate limits on an endpoint, per client.

Package app — records: the product's data as modules and the Studio reach it.

Package app — redaction: what a value shows once its secrets are hidden.

Package app — the register of processing: each store's line.

Package app — replacements: a test's substitute for an operation.

Package app — the HTTP request and response an endpoint can reach.

Package app — retention: how long a store keeps its personal data.

Package app — the retention of a store in a running app.

Package app — one pass of retention: what is due is erased or deleted.

Package app — record revisions: a store's previous versions of each record.

Package app — what changed between two versions, and a version written back.

Package app — where a store's versions are kept, engine by engine.

Package app — what the privacy rules ask of a record's versions.

Package app — a record's version written back.

Package app — what the Studio sees of a record's versions.

Package app — the revisions command: a version of a record written back.

Package app — binaries and their process roles: one executable, several Apps, and the versioned contracts between them (D22).

Package app — schemas: the JSON Schema of an operation's types.

Package app — scopes: the parts of a singleton's lock or of a socket's path that are only known at the start — the user, the executable, a configuration directory —, so one declaration keeps one process per user, per installed copy or per configuration.

Package app — the current user outside Windows: the process's UID, read from the kernel. os/user would ask libc through cgo — linking the program dynamically — or read /etc/passwd, for a number the kernel gives at once.

Package app — the current user on Windows: its SID, from os/user, which reads the process token there without cgo.

Package app — sealing at rest: members sealed where the framework keeps them.

Package app — where the data keys are, against what they seal.

Package app — cryptographic erasure: a data key destroyed.

Package app — a document as it lies at rest, sealed member by member.

Package app — messages at rest: a queue's members sealed.

Package app — a store that seals: its records as they rest.

Package app — a store that seals, as the framework's port sees it.

Package app — secrets: the values the product declares and the operator provides.

Package app — the secrets command.

Package app — where the secrets are kept, per environment.

Package app — services: a bounded context and the building blocks it owns.

Package app — settings: a value the environment gives a service.

Package app — the interfaces of settings: the values a setting holds, and what the framework reads of a declaration.

Package app — frontends: static assets the product serves.

Package app — stores: a typed, keyed collection of entities.

Package app — the engines a store runs on: memory, files or a database.

Package app — stores on SQL: the SDK's document store over a database.

Package app — the framework's own migrations on a database.

Package app — telemetry: the numbers a product reports, exported on a private socket when the deployment asks for them (ADR 0149).

Package app — topics and subscriptions: asynchronous messages of one type.

Package app — transactions: app.Transact and its levels.

Package app — a transaction on a database, the database's own.

Package app — the data's writer turn, where there are no transactions.

Package app — a transaction writes one database: the dev warning.

Package app — watches: how a module hears the writes of a product's marked fields.

Package app — a watch in a running app: its queue and its consumer.

Package app — the sentences the framework says: English from its own table, the languages after it from framework/app/studio's catalogues.

Package app — workflows: a state machine over the entities of one store.

Package app — the ports the workflow engine reaches a store through.

Index

Constants

View Source
const (
	// EnvDev serves the Studio, analyzes the source and binds loopback. Only
	// `kit dev` or an explicit <PRODUCT>_ENV=dev turns it on.
	EnvDev = "dev"
	// EnvProduction is the default: no Studio, no source, no analysis.
	EnvProduction = "production"
)

The environments.

View Source
const (
	WireInvalid     = "invalid_argument"
	WireNotFound    = "not_found"
	WireConflict    = "conflict"
	WireUnauth      = "unauthenticated"
	WireForbidden   = "permission_denied"
	WireTooLarge    = "too_large"
	WireUnsupported = "unsupported_media_type"
	WireRateLimited = "rate_limited"
	WireUnavailable = "unavailable"
	WireTimeout     = "deadline_exceeded"
	WireCanceled    = "canceled"
	WireInternal    = "internal"
)

The wire codes a caller branches on: an Error's Code, snake_case, kept byte-for-byte from the platform's former runtime (ADR 0147 §6). They are named Wire*, not Code*, because Code* names a dotted-quad errs.Code in the SDK and the registry audit holds every Code* constant to that.

View Source
const (
	CodeStoreDecode  errs.Code = 0x00_04_02_01 // 0.4.2.1 — a stored entity no longer decodes into its type
	CodeStoreEncode  errs.Code = 0x00_04_02_02 // 0.4.2.2 — an entity does not encode to JSON
	CodeStorePersist errs.Code = 0x00_04_02_03 // 0.4.2.3 — the store file could not be written
	CodeStoreLoad    errs.Code = 0x00_04_02_04 // 0.4.2.4 — the store file could not be read at start
	CodeStoreIndex   errs.Code = 0x00_04_02_05 // 0.4.2.5 — the stored entities break a unique index, or a key function panicked
	CodeTopicEncode  errs.Code = 0x00_04_02_06 // 0.4.2.6 — a message does not encode to JSON
	CodeTopicPublish errs.Code = 0x00_04_02_07 // 0.4.2.7 — a subscription's queue refused a message
	CodeQueueOpen    errs.Code = 0x00_04_02_08 // 0.4.2.8 — a subscription's queue could not be opened
	CodeUndecodable  errs.Code = 0x00_04_02_09 // 0.4.2.9 — a delivered message does not decode
	CodeHandlerPanic errs.Code = 0x00_04_02_0A // 0.4.2.10 — a subscription handler panicked
	CodeNotMounted   errs.Code = 0x00_04_02_0B // 0.4.2.11 — a subscription's topic is not mounted in the app
	CodeWorkflowLoad errs.Code = 0x00_04_02_0C // 0.4.2.12 — a workflow's bookkeeping could not be read at start

	// A workflow hook panicked: an OnEnter hook fails its transition with it,
	// an OnTransition hook is reported and the transition stands.
	CodeWorkflowHookPanic errs.Code = 0x00_04_02_0D // 0.4.2.13

	// An OnEnter hook changed the state its entity was entering, or the
	// entity's key: the transition is refused.
	CodeWorkflowHookChange errs.Code = 0x00_04_02_0E // 0.4.2.14

	// An OnEnter hook fired its own workflow: refused, where it would wait
	// for itself.
	CodeWorkflowReentrant errs.Code = 0x00_04_02_0F // 0.4.2.15
	CodeAppConfig         errs.Code = 0x00_04_02_10 // 0.4.2.16 — the app's configuration cannot be applied
	CodeAppRunning        errs.Code = 0x00_04_02_11 // 0.4.2.17 — the app, or one of its services, is already running
	CodeAppListen         errs.Code = 0x00_04_02_12 // 0.4.2.18 — the app could not listen on its address

	// Mailers.
	CodeMailConfig errs.Code = 0x00_04_02_13 // 0.4.2.19 — <PRODUCT>_SMTP_URL cannot be used
	CodeMailEncode errs.Code = 0x00_04_02_14 // 0.4.2.20 — a mail does not encode for the outbox
	CodeMailQueue  errs.Code = 0x00_04_02_15 // 0.4.2.21 — the outbox could not be opened, or refused a mail

	// Deprecated: the framework no longer returns it. The outbox is the SDK's mail
	// spool, which dead-letters a record that does not decode itself
	// (MessageUndecodable, in pkg/v1/app/mail/spool).
	CodeMailUndecodable errs.Code = 0x00_04_02_16 // 0.4.2.22
	CodeMailPanic       errs.Code = 0x00_04_02_17 // 0.4.2.23 — a delivery attempt panicked

	// Loops.
	CodeLoopPanic      errs.Code = 0x00_04_02_18 // 0.4.2.24 — a loop's function panicked
	CodeLoopNotMounted errs.Code = 0x00_04_02_19 // 0.4.2.25 — a loop wakes on a topic whose service is not mounted in the app
	CodeLoopStart      errs.Code = 0x00_04_02_1A // 0.4.2.26 — a hand-written loop's supervisor refused to start

	// Secrets.
	CodeSecretStore   errs.Code = 0x00_04_02_1B // 0.4.2.27 — the environment's secret store cannot be used: <PRODUCT>_SECRETS, <PRODUCT>_SECRETS_KEY
	CodeSecretMissing errs.Code = 0x00_04_02_1C // 0.4.2.28 — a declared secret is found nowhere
	CodeSecretRead    errs.Code = 0x00_04_02_1D // 0.4.2.29 — a secret's store refused to read it
	CodeSecretRotate  errs.Code = 0x00_04_02_1E // 0.4.2.30 — a generated secret could not be made or rotated

	// Databases, ADR 0004.
	CodeDatabaseOpen        errs.Code = 0x00_04_02_1F // 0.4.2.31 — a database's engine could not open its pool
	CodeDatabaseMigrate     errs.Code = 0x00_04_02_20 // 0.4.2.32 — a database's migrations did not apply, or wait for `migrate up`
	CodeDatabaseUnavailable errs.Code = 0x00_04_02_21 // 0.4.2.33 — a database did not answer
	CodeDatabaseConfig      errs.Code = 0x00_04_02_22 // 0.4.2.34 — a database's URL or tuning cannot be used
	CodeDatabaseVersion     errs.Code = 0x00_04_02_4C // 0.4.2.76 — a database's server is below the framework's floor: MySQL 8.0.1, MariaDB 10.6

	// A transaction wrote a store another database keeps than the one it
	// belongs to — the data directory counting as one: refused, Invalid.
	CodeTransactionSpan errs.Code = 0x00_04_02_42 // 0.4.2.66

	// Privacy (ADR 0006).
	CodePrivacyKey     errs.Code = 0x00_04_02_23 // 0.4.2.35 — the framework's index key cannot be read, made or derived
	CodePrivacyErase   errs.Code = 0x00_04_02_24 // 0.4.2.36 — an erasure failed: the store's Anonymise function panicked
	CodePrivacyJournal errs.Code = 0x00_04_02_25 // 0.4.2.37 — the privacy journal cannot be read or written
	CodePrivacyData    errs.Code = 0x00_04_02_26 // 0.4.2.38 — the privacy command cannot open the data: the product runs, or keeps it in memory

	// History (ADR 0007).
	CodeHistoryLoad  errs.Code = 0x00_04_02_27 // 0.4.2.39 — a store's history cannot be opened at start
	CodeHistoryWrite errs.Code = 0x00_04_02_28 // 0.4.2.40 — a store's history cannot be written: the record's write is refused with it
	CodeHistoryRead  errs.Code = 0x00_04_02_29 // 0.4.2.41 — a store's history cannot be read
	CodePasswordHash errs.Code = 0x00_04_02_2A // 0.4.2.42 — a password cannot be hashed, or a stored hash cannot be read

	// Commands and queries.
	CodeCommandReentrant errs.Code = 0x00_04_02_2B // 0.4.2.43 — a handler dispatched its own command with its own key: refused, never a deadlock
	CodeCommandKey       errs.Code = 0x00_04_02_2C // 0.4.2.44 — a command's key could not be held
	CodeCommandQueue     errs.Code = 0x00_04_02_2D // 0.4.2.45 — a queued command's queue could not be opened, or refused a dispatch
	CodeCommandEncode    errs.Code = 0x00_04_02_2E // 0.4.2.46 — a queued command's input does not encode for its queue

	// Watches (ADR 0008).
	CodeWatchQueue  errs.Code = 0x00_04_02_2F // 0.4.2.47 — a watch's queue could not be opened, or refused a notice: lost when the write stands, the transaction's failure when it joined it
	CodeRecordsKeys errs.Code = 0x00_04_02_4D // 0.4.2.77 — Records.Keys asked by a caller none of whose module's watches the store feeds: refused, Forbidden
	CodeWatchReplay errs.Code = 0x00_04_02_51 // 0.4.2.81 — `watch replay` names no watch of the product: refused, NotFound

	// Sealing at rest (ADR 0006, step 3).
	CodeSealKey    errs.Code = 0x00_04_02_3D // 0.4.2.61 — the framework's data keys cannot be reached: data-key, or the framework's own store of data keys
	CodeSealWrite  errs.Code = 0x00_04_02_3E // 0.4.2.62 — a member could not be sealed: the write is refused, nothing changed
	CodeSealOpen   errs.Code = 0x00_04_02_3F // 0.4.2.63 — a sealed member does not open: altered, moved, or its data key does not unwrap
	CodeSealRewrap errs.Code = 0x00_04_02_40 // 0.4.2.64 — data-key's rotation could not re-wrap every data key: the old version is kept
	CodeSealShred  errs.Code = 0x00_04_02_41 // 0.4.2.65 — an erasure could not destroy a data key: the erasure's rewrite stands

	// Revisions (ADR 0007 §3).
	CodeRevisionRead   errs.Code = 0x00_04_02_43 // 0.4.2.67 — a record's versions cannot be read, or two of them compared
	CodeRevisionWrite  errs.Code = 0x00_04_02_44 // 0.4.2.68 — a record's versions cannot be rewritten: an erasure's, a hold's, a rollback's
	CodeRevisionDecode errs.Code = 0x00_04_02_45 // 0.4.2.69 — a version no longer decodes into the store's type: Revisions, Revision and Restore refuse it

	// The trash (kitsunium/sdk ADR 0178 C4).
	CodeTrash     errs.Code = 0x00_04_02_4E // 0.4.2.78 — a store's trash cannot be read or emptied: what the product deleted stays in it until a later purge
	CodeTrashList errs.Code = 0x00_04_02_52 // 0.4.2.82 — the Studio's trash list names no store of the product (NotFound) or one without app.Revisions (Invalid)

	// The framework's own signals and refusals, typed since the move into the
	// SDK (rule 2: no fmt.Errorf, no errors.New in production code).
	CodeJobOverlapped  errs.Code = 0x00_04_02_31 // 0.4.2.49 — a scheduled job's run was skipped: the previous one is still going
	CodeHistoryMoved   errs.Code = 0x00_04_02_32 // 0.4.2.50 — an undo ended: another write changed the history since
	CodeNotSource      errs.Code = 0x00_04_02_33 // 0.4.2.51 — the source endpoint's name holds no regular file
	CodePasswordMoved  errs.Code = 0x00_04_02_34 // 0.4.2.52 — a password write ended: another write changed the hash since
	CodeNothingToErase errs.Code = 0x00_04_02_35 // 0.4.2.53 — an erasure's update found nothing to erase
	CodeKeyRekeyed     errs.Code = 0x00_04_02_36 // 0.4.2.54 — an erasure's update would change a key that depends on personal data
	CodeMigrateRefused errs.Code = 0x00_04_02_37 // 0.4.2.55 — the migrate command cannot do what it was asked
	CodeCatalogue      errs.Code = 0x00_04_02_38 // 0.4.2.56 — the framework's own message catalogues disagree
	CodeRetentionPanic errs.Code = 0x00_04_02_39 // 0.4.2.57 — a store's retention function panicked

	// The signals the framework ends one of its own steps with and catches itself — a
	// caller never receives one —, typed when the SDK made rule 2 a gate
	// (make guard), where they had been stdlib errors.
	CodeSealErased         errs.Code = 0x00_04_02_46 // 0.4.2.70 — a sealed value's data key is destroyed: the framework reads the value as its zero value
	CodeSealKeyMoved       errs.Code = 0x00_04_02_47 // 0.4.2.71 — a data key's replacement ended: another write changed the wrapped key since
	CodeResealInPlace      errs.Code = 0x00_04_02_48 // 0.4.2.72 — a write that changed nothing ended, for the record to be sealed again in place: no version
	CodeResealMoved        errs.Code = 0x00_04_02_49 // 0.4.2.73 — a reseal in place ended: another write changed the record since
	CodeWorkflowNotInPlace errs.Code = 0x00_04_02_4A // 0.4.2.74 — a transition's write in place ended: it changes more than the state, so it makes a version
	CodeTransactionPanic   errs.Code = 0x00_04_02_4B // 0.4.2.75 — a transaction's function, or a function held until its commit, panicked: the panic continues
)

The framework's own failures carry SDK error codes in 0.4.2.* — layer 4 is the framework's, PP 2 this package (ADR 0147 §3) — so a product keeps the whole application range 0x40–0x7F for its own codes. Match them with errs.HasCode. Their Public text is the only part that may reach a caller; none of them is a 4xx, so a caller only ever reads "internal error".

View Source
const (
	// CodeDataCopy refuses a data copy (ADR 0178 C7): a database the product
	// does not declare or cannot open, data kept in memory, a product that
	// answers on its address, a table of the database that already holds a
	// row; or reports a copy done whose file queues could not be moved aside.
	CodeDataCopy errs.Code = 0x00_04_02_4F // 0.4.2.79 — the data copy command cannot do what it was asked: no such database, a product running, a table that holds rows

	// CodeDataCopyDiverged reports a data copy whose fingerprints differ on
	// both sides of a table, or whose records do not open through the framework once
	// copied: the copy's transaction is rolled back, and the first key that
	// differs is named.
	CodeDataCopyDiverged errs.Code = 0x00_04_02_50 // 0.4.2.80 — a data copy's fingerprints differ: the copy is rolled back, the first key that differs named
)
View Source
const (
	// CodeRenameTarget refuses a start that meets what a former release
	// kept under the tool's name — a table, the privacy service's
	// directory, the dev data directory — while its present name is taken
	// too (ADR 0180 step 5): the framework never replaces one with the
	// other, so nothing is moved and both are named. What rests under the
	// former name stays as it was, readable by the release that wrote it.
	CodeRenameTarget errs.Code = 0x00_04_02_56 // 0.4.2.86 — a name at rest the framework moves to its present name is taken too: nothing is moved, both are named

	// CodeRenameFailed refuses a start whose move of a name at rest to its
	// present name did not complete (ADR 0180 step 5): a directory the
	// system would not rename, a statement the database did not run. What
	// was not moved stays under its former name, readable as it was, and
	// the next start resumes the move.
	CodeRenameFailed errs.Code = 0x00_04_02_57 // 0.4.2.87 — a name at rest could not be moved to its present name: the former stays readable, the next start resumes

	// CodeRetryIn marks a failure a handler returned through [RetryIn]: the
	// queue of the subscription, the watch or the queued command that ran
	// it hands the message back with the delay it asked for, the delivery
	// counted. It joins the trail of an SDK cause, whose own code a dead
	// letter records; any other cause is recorded under it.
	CodeRetryIn errs.Code = 0x00_04_02_58 // 0.4.2.88 — a handler asked for its message to be retried after a delay of its own

	// CodeGaveUp marks a declared loop the framework stopped running: its
	// runs failed more often than its [GiveUpAfter] allows, and the product
	// is no longer live until the process restarts. A consumer and a
	// hand-written loop give up through the SDK's supervisor, whose own
	// code — lifecycle's SupervisorGaveUp — their liveness check answers.
	CodeGaveUp errs.Code = 0x00_04_02_59 // 0.4.2.89 — a supervised component failed too often and the framework gave it up

	// CodeOTLPExport refuses a start whose OTLP export ([OTLP]) cannot be
	// opened — its queue, its collector's endpoint, its headers —, says a
	// stop that did not let it flush within its budget, marks a batch the
	// product's own sender refused for good, and refuses the otlp command
	// a queue it cannot read or a dead letter it does not hold.
	CodeOTLPExport errs.Code = 0x00_04_02_5A // 0.4.2.90 — the OTLP export could not be opened, or did not stop within its budget

	// CodeTransactionNested refuses a Transact inside another that asks for
	// an isolation level or read-only ([Serializable], [RepeatableRead],
	// [ReadOnly]): the inner level is a savepoint of the outer
	// transaction, which can change neither.
	CodeTransactionNested errs.Code = 0x00_04_02_5B // 0.4.2.91 — a nested app.Transact was given an isolation level or read-only

	// CodeTransactionReadOnly refuses a store's write inside a transaction
	// opened with [ReadOnly], on every backend, before it reaches the
	// database.
	CodeTransactionReadOnly errs.Code = 0x00_04_02_5C // 0.4.2.92 — a store write inside a transaction opened read-only
)
View Source
const (
	// OTLPTraces names the trace signal: an OTLP/JSON
	// ExportTraceServiceRequest.
	OTLPTraces string = plug.OTLPTraces
	// OTLPMetrics names the metrics signal: an OTLP/JSON
	// ExportMetricsServiceRequest.
	OTLPMetrics string = plug.OTLPMetrics
)

The two signals an export forwards, as an OTLPBatchValue names them.

View Source
const CodeCLIFailed errs.Code = 0x00_04_02_3B // 0.4.2.59 — a CLI command exited with a non-zero status

CodeCLIFailed marks the span of a CLI command that exited non-zero.

View Source
const CodeMigrationIrreversible errs.Code = 0x00_04_02_54 // 0.4.2.84 — a migration's Down is app.Irreversible: reversing it takes a new forward migration

CodeMigrationIrreversible refuses the Down of a migration declared irreversible with Irreversible (ADR 0179 step 10): what it changed is undone by a new forward migration, never by migrate down.

View Source
const CodeProfileRead errs.Code = 0x00_04_02_30 // 0.4.2.48

CodeProfileRead reports a profile of the process that could not be written or read back. It is in the framework's 0.4.2.* range.

View Source
const CodeRenamed errs.Code = 0x00_04_02_55 // 0.4.2.85 — a runtime name the framework no longer reads was met at the start: the name that replaces it is said

CodeRenamed refuses a start that meets a runtime name the framework no longer reads (ADR 0180 §3, Q12): an operator's variable still named KIT_<NAME> where the framework reads <PRODUCT>_<NAME>, a struct field still tagged kit:"…" where it reads app:"…". Ignored, the first would start the product on a default — an empty data directory —, the second would store a personal field in clear. The refusal is a DiagnosticsError naming every old name met and the name that replaces it, all at once; it carries this code, which errs.HasCode reads through it. The refusal holds for two minors, then the old names are forgotten.

View Source
const CodeScopeUnknown errs.Code = 0x00_04_02_3C // 0.4.2.60 — a scope's value could not be read

CodeScopeUnknown reports a scope whose value could not be read at the start: no current user, no executable path, no configuration directory.

View Source
const CodeSingletonHeld errs.Code = 0x00_04_02_3A // 0.4.2.58 — another process of the app holds its singleton lock

CodeSingletonHeld reports a start refused because another process of the app holds its singleton lock.

View Source
const CodeSubsystemMissing errs.Code = 0x00_04_02_53 // 0.4.2.83 — a declaration needs a subsystem whose registration package the program does not import

CodeSubsystemMissing refuses a start whose declarations need a subsystem the program does not import (ADR 0179 §6): a store or a database without framework/app/store, a server without framework/app/server. The refusal is a DiagnosticsError naming every such declaration and the package to import, all at once; it carries this code, which errs.HasCode reads through it.

View Source
const DefaultMaxBody int64 = 1 << 20

DefaultMaxBody is the largest request body an endpoint reads, unless the endpoint says otherwise with MaxBody.

Variables

View Source
var (
	// PerUID is the current user: one process per user of the machine (its
	// UID on Unix, its SID on Windows).
	PerUID = ScopeValue{/* contains filtered or unexported fields */}

	// PerExecutable is the running executable's path, links resolved: one
	// process per installed copy of the binary.
	PerExecutable = ScopeValue{/* contains filtered or unexported fields */}

	// PerConfigDir is the user's configuration directory (os.UserConfigDir):
	// one process per configuration home.
	PerConfigDir = ScopeValue{/* contains filtered or unexported fields */}
)
View Source
var Privacy = &Service{name: privacyService, doc: privacyDoc, ids: map[string]bool{}}

Privacy is the framework's own service: the data keys, the legal holds and the privacy journal of an app that keeps personal data or seals what it keeps. The framework mounts a copy of it — named app.privacy, a name no service of a product can take — in every app whose stores keep personal data or declare a retention, or that keeps sealed values at rest, before the product's services. Its stores stay in the data directory unless the product places them (ADR 0004): app.Keeps(app.Privacy). On a SQLite database its data keys stay in the data directory all the same — a key is written apart from every transaction, and the file's one writer may be held by the transaction of the write that needs it —; its holds and its journal go there. A product never mounts it itself.

Functions

func After

func After(ctx context.Context, d time.Duration) <-chan time.Time

After is time.After on the app's clock.

func AuthData

func AuthData[D any](ctx context.Context) (D, bool)

AuthData returns what the auth handler said about the caller, and whether there is a caller whose data has type D.

func Bind

func Bind[Req, Resp any](port *PortService[Req, Resp], op Operation[Req, Resp]) interface {
	AppConfigurer
	MountConfigurer
}

Bind makes the app's port call op, whatever implements the port and whatever its fallback: an endpoint — or, with ADR 0005, a command or a query — of the port's types. A binding is code, the same in every environment; a binding to a port, or to a service the app does not mount, is refused when the app starts. Given twice for one port, the last wins.

a := app.NewApp("vigie", desk.Service, posts.Service).With(app.Bind(desk.Enforcer, posts.EnforceAPI))

It is an option of Mount too, under the same name: there it binds a port of the module mounted, and another is refused.

a := app.NewApp("shop", posts.Service).With(app.Mount(moderation.Module, app.Bind(moderation.Enforcer, posts.EnforceAPI)))

func Catalog

func Catalog() []model.Mechanic

Catalog lists the generic mechanics the framework offers: the building blocks a node composes rather than implements, each one backed by an SDK package. It is part of every graph, so the Studio — and an AI agent reading the graph — knows what can be added to a product and the exact code that adds it.

func ClearCookie

func ClearCookie(ctx context.Context, name string)

ClearCookie tells the browser to forget the cookie called name.

func ClientIP

func ClientIP(ctx context.Context) string

ClientIP returns the address of the client of the request ctx serves: the connection's peer, or — only when <PRODUCT>_TRUST_PROXY=on — the last address of X-Forwarded-For, the one the trusted proxy appended. It is "" outside an HTTP request.

func EnableOTLP added in v0.52.0

func EnableOTLP()

EnableOTLP makes the framework's own export code reachable: the settings, the component, the readiness and the stop of the OTLP export a product declares. framework/app/otlp calls it as it is imported; a product never does.

func EnableServer

func EnableServer()

EnableServer makes an app in the server profile serve HTTP. framework/app/server calls it as it is imported; a product never does.

func EnableStores

func EnableStores()

EnableStores makes the framework's own store code reachable: what the framework mounts, starts and answers for the stores and databases a product declares. framework/app/store calls it as it is imported; a product never does.

func EnableStudio

func EnableStudio()

EnableStudio makes a serving app in dev mount the Studio's routes, which framework/app/studio plugs in (plug.Studio), over a host of the app. framework/app/studio calls it as it is imported; a product never does.

func HoldSubject

func HoldSubject(ctx context.Context, reason string, ids ...string) error

HoldSubject places a legal hold on every record whose subject is one of ids, in every store of the app: an authority's request about a person. It holds the records they have now; a record written later is not held. reason is kept as Store.Hold keeps it.

func InMemory

func InMemory() interface {
	StoreConfigurer
	AppConfigurer
}

InMemory keeps data in memory. On an app, it overrides the data directory for every store and queue — <PRODUCT>_DATA_DIR, and a DataDir given before it: what a test wants. On a store, it keeps that store in memory even when the app has a data directory.

func Irreversible

func Irreversible(ctx context.Context, ex MigrationExecutor) error

Irreversible is the Down of a migration that cannot be reversed, said out loud rather than left nil: it refuses, with CodeMigrationIrreversible, and `<product> migrate down` below its version stops there.

func Log

func Log(ctx context.Context) logger.Logger

Log returns the app's logger, for the node ctx runs inside. In dev, every record it writes inside a span is also kept beside that span, so the Studio shows a request's logs with its trace.

func Meter added in v0.54.0

func Meter(ctx context.Context) metrics.FullMeter

Meter is the product's own meter, for the app ctx runs inside: the counters, gauges and histograms it makes land in the OTLP export's metrics stream, under the app's resource and an instrumentation scope named after the app, collected every otlp-interval beside the framework's own. Without a running export — none declared, the export off, a CLI command, or outside an app — it is a meter that keeps nothing. An instrument is found again by its name and attributes, so a handler may ask for it at each call; its Collect is the export's.

func Migrations

func Migrations(m ...MigrationValue) interface {
	DatabaseConfigurer
	ModuleConfigurer
}

Migrations are migrations, the framework's own [MigrationValue]s (ADR 0179 step 10), run when the database starts (<name>-migrate: start) or by `<product> migrate up`, the framework's own first, each set under its own version table — and so its own lock. On MySQL a DDL statement commits by itself: write one per migration. The store subsystem runs them: a program that declares a database imports framework/app/store, as its engine module does.

Given to Database, they are the product's, under schema_migrations. Given to NewModule, they are the module's (ADR 0008), for the data it keeps outside the framework's stores: run on the database that keeps the module, under <module>_migrations.

func NewID

func NewID(prefix string) string

NewID returns a new identifier with the given prefix: a TypeID, time ordered, like "todo_01k5zq7m3xe8tvbfg0s7zr4w6c". The prefix must be one to sixty-three lower-case letters, with '_' only between two letters; any other prefix is a programming error and panics.

func NewTicker

func NewTicker(ctx context.Context, d time.Duration) clock.Ticker

NewTicker is a ticker of period d on the app's clock, for a loop written by hand (Service.Go): a test's manual clock drives it like every timer the framework runs itself, where a time.Ticker would tick on the wall clock whatever the test does. It panics on a non-positive d, as time.NewTicker does.

func Now

func Now(ctx context.Context) time.Time

Now returns the app's time: the system clock, or the clock a test gave with Clock. Product code that compares with a deadline reads it rather than time.Now, so that a test's manual clock reaches it.

func RegisterConfigFormat

func RegisterConfigFormat(ext, format string)

RegisterConfigFormat lets configuration files of extension ext be read with the SDK codec format. framework/app/config/yaml and config/toml call it as they are imported; a product never does.

func RetryIn added in v0.50.0

func RetryIn(d time.Duration, cause error) error

RetryIn marks cause as a failure to retry in d, no sooner: returned from the handler of a subscription, a watch or a queued command, it hands the message back to its queue with that delay, in place of the queue's own — half a second after a first failure, doubling, never more than a minute —: a downstream that answered with a Retry-After, a resource that frees at a known instant. The delivery counts as an attempt: a message that used its MaxDeliveries is dead-lettered with cause, the delay unused. A negative d is read as zero.

if resp.StatusCode == http.StatusTooManyRequests {
	return app.RetryIn(30*time.Second, errThrottled)
}

cause keeps its own code, reason and words — what a dead letter records — and CodeRetryIn joins its trail; a cause that is no error of the SDK's, or none, is recorded as CodeRetryIn itself. Outside a consumer's handler it is an error like any other.

func ScopeKey

func ScopeKey(scopes ...ScopeValue) (string, error)

ScopeKey is the name the scopes give in this process: 16 hex digits of the SHA-256 of their values. A client computes the key the daemon computed from the same declaration, and so finds its lock or its socket.

func SetCookie

func SetCookie(ctx context.Context, c *http.Cookie)

SetCookie adds a Set-Cookie header to the HTTP response of the endpoint ctx serves. The framework fills what a cookie left zero with safe defaults — Path "/", HttpOnly, SameSite Lax, and Secure outside dev — so a session cookie is hardened unless the code says otherwise. In an in-process EndpointService.Call there is no response: the cookie is dropped.

HttpOnly is always set, and Secure always outside dev (and in dev for SameSite=None, which browsers require): a boolean has no "unset" a default could fill, and the framework chooses the hardened reading. A cookie set more than once in one response keeps its last value, and cookies set before an error are sent with the error — clearing a stale session on a 401 works.

func SocketPathFor

func SocketPathFor(product, service, name string, scopes ...ScopeValue) (string, error)

SocketPathFor is the socket of the listener name of service in product — the binary's name for a role of one, else the app's —, placed by default and named after scopes (SocketPer): the one rule, for a client of another process role that does not import the daemon's declarations (D22) — it names the three words and the scopes its contract states. The service is its qualified name for a module's.

func Stop

func Stop(ctx context.Context) bool

Stop asks the app ctx runs in to end its run: Run returns as it does on a signal, its stop draining every component — a daemon told "stop" by its client, on its listener, ends itself so. It returns at once, before the stop; it reports false when ctx runs in no app — a handler's, a loop's, a command's context does — or the app does not run.

func Transact

func Transact(ctx context.Context, fn func(context.Context) error, opts ...TransactOption) error

Transact runs fn in one transaction and returns fn's error: nil commits what fn wrote, an error undoes it — a panic too, which then continues.

err := app.Transact(ctx, func(ctx context.Context) error {
	if _, err := Cases.Update(ctx, id, closeCase); err != nil {
		return err // what this function wrote is undone
	}
	if err := Decisions.Insert(ctx, d); err != nil {
		return err
	}
	return Changes.Publish(ctx, Closed{ID: id}) // held: queued at the commit
})

A transaction belongs to one database: the one its first store call reaches — at its first call on a database, at its first write on the data directory, which counts as one, memory with it. Inside it, a store of another database is read as usual; writing one is refused (Invalid, CodeTransactionSpan). A Transact inside another is a savepoint: its failure undoes its own writes, and the outer transaction goes on if its caller handles the error.

What leaves the process waits for the outermost commit: a topic's publish, a queued command's dispatch, a mail — whose outbox ID Mailer.Send returns at once —, a watch's notice, the store's write hooks that wake workflows and loops, a workflow's OnTransition hooks. Each is checked when it is made, so a caller's mistake returns to the caller; a rollback drops them, and a savepoint's rollback its own. A publish, a dispatch, a mail or a notice whose queue lives on the transaction's database — the one that keeps the service of its subscription, command, mailer or watch, by a store's rules — is written in the transaction, just before it commits, a mail sealed under the framework's data-key: a queue that refuses it fails the transaction, every write undone, and Transact returns the refusal. Every other one leaves after the commit: one a queue refuses then is logged and reported, and the commit stands; a process that dies between the commit and the release loses what was held.

On a database the transaction is the database's own, at its default isolation unless opts ask for another: Serializable or RepeatableRead opens it at that level, ReadOnly tells the database it only reads, and a store's write inside it is refused (Invalid, CodeTransactionReadOnly) on every backend. Options go on the outermost Transact: a nested one given any is refused (Invalid, CodeTransactionNested), its savepoint able to change neither. On the data directory and in memory the framework keeps its own: it takes the data's writer turn — one transaction at a time; a write outside any waits for it, and such writes share the turn among themselves —, its writes land at once, and an error writes each entity's previous value back, newest first. It is atomic against an error, not against a crash, and a reader may see a write before its commit. A store kept in memory by InMemory — a cache — is in no transaction: its writes land and stay.

A command's handler runs in one already (NoTransaction opts out), and so does a workflow's transition.

func UserAgent

func UserAgent(ctx context.Context) string

UserAgent returns the User-Agent of the request ctx serves, clipped.

func WithUser

func WithUser[D any](ctx context.Context, uid UID, data D) context.Context

WithUser returns ctx acting as the given user, with data: what a job, a loop or a test uses to call an Auth endpoint in-process on someone's behalf.

Types

type ActivityHandler

type ActivityHandler struct {
	// contains filtered or unexported fields
}

ActivityHandler is a declared activity of a service: a function that says whether the product is busy. A daemon's IdleStop waits until every activity of its mounted services says false AND no connection is open on its listeners.

type AnalyzeFunc

type AnalyzeFunc func(ctx context.Context, dir string, modules []string) (*model.Graph, error)

AnalyzeFunc reads the product's source — the Go module rooted at dir, and the packages that declare the modules it mounts — into a graph the runtime merges into its own: the edges found in handler bodies, each function's steps. The framework links no analyzer (ADR 0147 §1): the platform's former runtime tool gives one, in dev, through Analyzer.

type App

type App struct {
	// contains filtered or unexported fields
}

App is the whole product: the services it mounts, served by one process. The same services can be mounted by several apps — one binary per service, or all of them in one — and each app draws exactly what it runs.

func NewApp

func NewApp(name string, services ...*Service) *App

NewApp declares the product named name, made of services. A module's services come with the module: App.With mounts it.

func (*App) Fields

func (a *App) Fields(m Mark) []FieldRefValue

Fields lists the fields of the app's stores that carry the mark, store by store in the order the app mounts them, each in its entity's order. Personal lists special fields too: they are personal data.

func (*App) Graph

func (a *App) Graph() *model.Graph

Graph returns the product as a diagram: every node the app mounts with the position of its declaration, the edges that exist by construction, the edges observed at runtime with their counters, and — in dev, once the static analysis has run — the edges found in the handlers' code with their call sites. It is what the Studio draws and what `<app> graph` prints.

func (*App) Handler

func (a *App) Handler() http.Handler

Handler returns the app's HTTP handler, once started: what httptest wraps.

func (*App) Main

func (a *App) Main(ctx context.Context, args []string) int

Main is the whole main function of a product:

func main() { os.Exit(App.Main(context.Background(), os.Args[1:])) }

It understands ten commands, so every binary of the framework can describe itself, be probed, say its configuration, manage its secrets, migrate its databases, answer for the personal data it keeps, replay what its watches hear and copy its data directory into a database without a shell or another tool in its image:

serve        run the product (the default)
graph        print the product graph: -format json|mermaid
healthcheck  exit 0 when the product running on <PRODUCT>_ADDR is ready
config       every setting: its value, never a secret's, and its origin
secrets      list the secrets, set one from standard input, rotate one
migrate      the databases' migrations: status, up, down SET VERSION
privacy      the register of processing, a person's export and erasure,
             the legal holds, the privacy journal, the retention
revisions    a record's version restored
watch        replay WATCH: one notice per record of each store that feeds it
data         the data directory copied into a database: data copy --to NAME,
             verified by fingerprints

An app whose port declares a Breaker understands breaker too: list the breakers, isolate PORT — open its breaker until it is reset — and reset PORT, kept in the data directory for every start and a running product. An app that declares an OTLP export understands otlp: dead-letters lists the batches its collector refused for good, never their payload, and replay puts them back into the live queue, or with --drop deletes them.

A product's CLI command runs when the first argument names it. A command declared with DefaultCommand runs when the first argument names no command at all — with every argument —, and when there is none, in serve's place: Main then never answers a usage error.

It returns the process exit status rather than exiting, so that deferred work runs and a test can call it.

func (*App) Name

func (a *App) Name() string

Name returns the product name.

func (*App) Records

func (a *App) Records(store string) (RecordsService, error)

Records returns the untyped port to the records of the store whose node ID is store: what the framework's export and erasure run on, and what a module reads and erases through. A store the app does not mount is a NotFound error.

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run starts the product, serves until ctx ends or the process receives SIGINT or SIGTERM, then drains it.

Goroutine lifecycle: one goroutine turns a signal into the run's cancellation; it ends when a signal arrives or when the run returns, which cancels the context it also watches.

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start brings the product up: it checks every declaration, opens the data directory, loads the stores, starts the consumers, the scheduler and the HTTP listener — in that order, through the SDK lifecycle, so a failure unwinds what already started. It returns once the product serves.

func (*App) Stop

func (a *App) Stop(ctx context.Context) error

Stop drains the product: readiness turns false, then the components stop in reverse order — the HTTP listener finishes its requests before the stores close.

func (*App) URL

func (a *App) URL() string

URL returns the base URL the app listens on, once started.

func (*App) With

func (a *App) With(opts ...AppConfigurer) *App

With returns a new app: the same product, with the options of a and then opts — a module among them is mounted. a itself is left as it was, so the options a test gives the product's App — an app.Set, an app.Replace — end with that test.

type AppConfigurer

type AppConfigurer interface {
	// contains filtered or unexported methods
}

AppConfigurer configures an app. Options win over the environment.

func Analyze

func Analyze(on bool) AppConfigurer

Analyze switches the in-process static analysis on or off in dev.

func Analyzer

func Analyzer(fn AnalyzeFunc) AppConfigurer

Analyzer runs fn in the background once the product serves, in dev and when KIT_ANALYZE allows it. Without one, the graph is the runtime's alone.

func Clock

func Clock(c clock.Timed) AppConfigurer

Clock makes the app read and wait on c instead of the system clock: a test drives timer transitions and jobs with a clock.ManualClock.

func ConfigFiles

func ConfigFiles(fsys fs.FS) AppConfigurer

ConfigFiles gives the framework the product's configuration files: config/config.<ext>, which every environment reads, and config/<env>.<ext>, the environment's own — <env> being <PRODUCT>_ENV as written, dev when kit dev runs the product — in JSON, YAML or TOML (json, yaml or yml, toml). A file maps the names of the settings to their values; it never holds a secret, and a key no service declares stops the start. The files belong in the binary:

//go:embed config
var configFiles embed.FS

a := app.NewApp("todo", ...).With(app.ConfigFiles(configFiles))

func DataDir

func DataDir(dir string) AppConfigurer

DataDir sets where stores and queues persist, instead of <PRODUCT>_DATA_DIR. Given to an app after InMemory, it wins: the last of the two says where the data is — so a test helper that runs the product in memory runs it on disk with the one option its test gives (apptest.NewDisk).

func Database

func Database(name string, engine Engine, opts ...DatabaseConfigurer) AppConfigurer

Database declares a database the product keeps its stores in, named name, on engine — an engine module's, which the product's main imports:

import (
	"github.com/kitsunium/sdk/framework/connectors/postgres"
	"github.com/kitsunium/sdk/framework/connectors/sqlite"
)

var App = app.NewApp("vigie", intake.Service, desk.Service, audit.Service).With(
	// Keeps every store no other database keeps.
	app.Database("database", postgres.Engine()),
	// The audit trail apart.
	app.Database("archive", sqlite.Engine(), app.Keeps(audit.Service)),
)

name is 1 to 49 lower-case letters, digits and dashes, not starting with "app-" nor "kit-", unique among the app's databases. It names the database's URL, the secret <name>-url — the variable <APP>_<NAME>_URL, or <APP>_<NAME>_URL_FILE naming a file that holds it, then the environment's secret store —, and its settings: <name>-max-open (10), <name>-max-idle (2), <name>-max-lifetime (30m), <name>-max-idle-time (0: none), <name>-timeout (5s), <name>-check-timeout (5s), <name>-tx-retries (3: how many times a transaction the database asks to run again runs again, 0 for never — Classifier), <name>-tx-retry-delay (50ms: the first wait before it does) and <name>-migrate (start, or manual). Outside dev the URL is required and writes its TLS mode; in dev a database without one leaves its stores in the data directory, and a SQLite database without one is the file <data>/<name>.sqlite. InMemory on the app opens none.

func DesignDigest

func DesignDigest(digest string) AppConfigurer

DesignDigest records the digest of the design the product's code was generated from — what the generated wiring passes —, so the telemetry handshake can say it and a tool attached to the product can tell a process built from another design.

func Env

func Env(env string) AppConfigurer

Env selects the environment, instead of <PRODUCT>_ENV.

func IdleStop

func IdleStop(d time.Duration) AppConfigurer

IdleStop makes a daemon stop itself once, for d, no connection was open on any of its listeners and no activity of its services (Service.Activity) said it was busy: a daemon launched on demand by its clients ends when they stop coming. It is refused outside the daemon profile.

func Listen

func Listen(addr string) AppConfigurer

Listen sets the listening address, instead of <PRODUCT>_ADDR or PORT. ":0" picks a free port; App.URL tells which.

func Logs

func Logs(w io.Writer) AppConfigurer

Logs sends the app's logs to w instead of standard error.

func Mount

func Mount(m *Module, opts ...MountConfigurer) AppConfigurer

Mount mounts module m with options: Prefix serves its routes elsewhere than "/<module>/", Bind chooses what one of its ports calls.

a := app.NewApp("vigie", identity.Service).
	With(app.Mount(moderation.Module, app.Prefix("/"), app.Bind(moderation.Enforcer, posts.EnforceAPI)))

A module is mounted once per app: mounted again, its prefix is the last one given, and its bindings join the others, the last of a port winning.

func OTLP added in v0.52.0

OTLP declares that the app forwards its traces and the metrics the framework derives from them to an OpenTelemetry collector over OTLP/HTTP: the spans the framework starts — the share otlp-sample-ratio keeps —, and per node and operation its runs, failures and durations (app.node.runs, app.node.failures, app.node.duration), with what the export itself holds and lost (app.otlp.*) and the product's own instruments (Meter). The export is on once its endpoint is set — the setting otlp-endpoint, OTLPEndpoint its default — or once OTLPSender replaces the network; a CLI command never exports. A program that declares it imports framework/app/otlp, or its start is refused, naming the import:

import _ "github.com/kitsunium/sdk/framework/app/otlp"

var App = app.NewApp("shop", catalog.Service).With(
	app.OTLP(app.OTLPEndpoint("http://collector:4318")),
)

Every value given here is the default of the setting that tunes it, which config/<env>.* and <PRODUCT>_OTLP_<KNOB> override. Given twice, the last wins.

func Profile

func Profile(p string) AppConfigurer

Profile selects the app's process profile: model.ProfileServer (the default), model.ProfileDaemon or model.ProfileCLI.

func Replace

func Replace[Req, Resp any](op Operation[Req, Resp], fn func(context.Context, Req) (Resp, error)) AppConfigurer

Replace runs fn in place of op's code, in the app it is given to: an endpoint's handler — or, with ADR 0005, a command's or a query's — or what a port calls. It replaces the code, not the mechanics: authentication, decoding, validation and the policies still run, and a replaced port needs no binding, so a module's tests run without a host. fn takes op's request and returns its response, or it does not compile.

A replaced run's span carries mock: replace, and runtime.mocks lists the replacement. It is for tests: the start refuses it in a program go test did not build — neither a production binary nor kit dev can carry one. Given twice for one operation, the last wins.

app := App.With(app.Replace(apps.AppAPI, func(ctx context.Context, in apps.IDInput) (apps.Profile, error) {
	return apps.Profile{ID: in.AppID}, nil
}))

func SecretStore

func SecretStore(store secret.Store) AppConfigurer

SecretStore keeps the secrets in store instead of the environment's: a test gives a store holding what its provided secrets hold. The environment's variables are still read first.

func Set

func Set(name string, value any) AppConfigurer

Set gives the setting name a value in code, over the environment and the files: a value of the setting's type, or its text as a variable would hold it. It is for tests.

func Singleton

func Singleton(scope string) AppConfigurer

Singleton keeps one process of the app alive per scope on this machine: the start takes an exclusive file lock named after scope in the app's runtime directory (pkg/v1/app/lock: flock, LockFileEx) and holds it until the process ends. A second process refuses to start with CodeSingletonHeld — for a daemon's client, the sign to talk to the one that runs. The kernel drops the lock with a dead process.

func SingletonPer

func SingletonPer(scopes ...ScopeValue) AppConfigurer

SingletonPer keeps one process of the app alive per value of scopes on this machine — per user, per installed executable, per configuration directory (PerUID, PerExecutable, PerConfigDir, PerEnv) —: the lock is named after the scopes' key (ScopeKey), computed at the start, and held as Singleton's is. A client that must find the process computes the same key from the same scopes.

func Studio

func Studio(on bool) AppConfigurer

Studio switches the Studio on or off in dev. It is always off in production.

func Telemetry

func Telemetry(path string, gids ...int) AppConfigurer

Telemetry exports the app's telemetry on the private socket at path — absolute, and short enough for a Unix socket (pkg/v1/proc/ipc) — admitting the groups gids besides the product's own account. It wins over <PRODUCT>_TELEMETRY.

type AttrsProvider

type AttrsProvider interface {
	Attrs() []authz.Attr
}

AttrsProvider is what the app's auth data says about the caller to the SDK's authz: the attributes Command.Allow and Query.Allow hand a policy — the caller's roles, its groups. Auth data that does not implement it gives none, and every Allow refuses: the start warns of it.

type Me struct{ Roles []string }

func (m Me) Attrs() []authz.Attr { return []authz.Attr{authz.AttrStrings("roles", m.Roles...)} }

The framework calls the caller a principal: in the framework, a subject is the person personal data is about (ADR 0006).

type AuthenticatorHandler

type AuthenticatorHandler[P, D any] struct {
	// contains filtered or unexported fields
}

AuthenticatorHandler is the app's authentication handler: it turns the credentials of a request — a session cookie, a bearer token — into a user, in front of every endpoint declared with Auth or AuthOptional. An app has at most one.

P is the credentials: a struct whose fields are tagged cookie:"…" or header:"…", decoded like an endpoint's request. D is what the handler says about the user; endpoints read it with AuthData.

func (*AuthenticatorHandler) ID

func (n *AuthenticatorHandler) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type Binary

type Binary struct {
	// contains filtered or unexported fields
}

Binary is one executable the product ships, made of process roles: a status line and its daemon are two roles of one binary, "statusline" and "statusline daemon". Each role is an App — its own services, its own profile — and two roles talk only through a declared, versioned contract (D22): a listener one serves and the other dials, never each other's code.

var Binary = app.NewBinary("statusline", "The status line of a Claude session.").
	Role("render", RenderApp).                // statusline ...
	Role("daemon", DaemonApp, "daemon").      // statusline daemon ...
	Talks("render", "daemon", "render/v1")

func main() { os.Exit(Binary.Main(context.Background(), os.Args[1:])) }

The package that declares a binary and its roles is wiring, generated from the design (D22).

func NewBinary

func NewBinary(name, doc string) *Binary

NewBinary declares the binary named name.

func (*Binary) Main

func (b *Binary) Main(ctx context.Context, args []string) int

Main runs the role the arguments select — the one whose args are the longest prefix of them, else the default role — with the rest of the arguments, through its App's Main. It returns the process exit status.

func (*Binary) Role

func (b *Binary) Role(name string, app *App, args ...string) *Binary

Role adds a process role named name, run by app, selected when the process's first arguments are args; the role without args is the default. It returns b, so roles chain.

func (*Binary) Talks

func (b *Binary) Talks(from, to, contractName string) *Binary

Talks declares that role from talks to role to through contract — "<name>/v<major>". It is the only edge two roles may have.

type BounceEvent added in v0.47.0

type BounceEvent struct {
	// Err is the last attempt's failure.
	Err error
	// Reply is the SMTP reply that failure carries, nil when the server
	// answered none — a relay that could not be reached, a timeout.
	Reply *textproto.Error
	// Message is the mail, as it was queued.
	Message mail.Message
	// QueuedAt is when it entered the outbox.
	QueuedAt time.Time
	// At is when it was given up, on the app's clock.
	At time.Time
	// Mailer is the mailer's node ID.
	Mailer string
	// ID is the mail's outbox ID.
	ID string
	// GivenUp is why: model.MailRejected, model.MailExpired or
	// model.MailExhausted.
	GivenUp string
	// Attempts is how many deliveries it got.
	Attempts int
}

BounceEvent is a mail its mailer gave up on: the receiving server refused it for good, it outlived its mailer's max-age, or its last attempt failed — GivenUp says which. It stays in the outbox as a dead letter.

Reply is the receiving server's own answer: the hook may read it, log it or act on it, but the framework never puts it in a message it shows — the Studio's mailbox keeps its code alone.

type BreakerConfigurer added in v0.49.0

type BreakerConfigurer interface {
	// contains filtered or unexported methods
}

BreakerConfigurer configures a Breaker.

func BreakerWindow added in v0.49.0

func BreakerWindow(size time.Duration, minCalls int, failureRatio float64) BreakerConfigurer

BreakerWindow makes a Breaker open on a rate rather than on failures in a row: once the last size of calls holds at least minCalls, a share of failures of failureRatio or more opens it. Its values are the defaults of the port's settings <service>-port-<name>-breaker-window, -breaker-window-calls and -breaker-window-ratio; a window of 0 is none, a ratio outside (0, 1] stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func HalfOpenTrials added in v0.49.0

func HalfOpenTrials(n int) BreakerConfigurer

HalfOpenTrials is how many trial calls in a row must succeed to close a Breaker once its open time is over — one by default. It is the default of the port's setting <service>-port-<name>-breaker-trials; fewer than one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OpenBackoff added in v0.49.0

func OpenBackoff(maxOpen time.Duration) BreakerConfigurer

OpenBackoff lengthens the open time of a Breaker that opens again without having closed: it doubles at each opening, from the breaker's open time up to max. It is the default of the port's setting <service>-port-<name>-breaker-open-max, 0 — the same open time every time — without it; a negative one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type BulkheadConfigurer added in v0.48.0

type BulkheadConfigurer interface {
	// contains filtered or unexported methods
}

BulkheadConfigurer configures a Bulkhead.

func BulkheadQueue added in v0.48.0

func BulkheadQueue(n int) BulkheadConfigurer

BulkheadQueue lets up to n calls beyond those a Bulkhead runs wait for a slot, each for its MaxWait: without a wait, or with n at 0, the overflow is refused at once, and a negative n stops the start. A call that finds the queue full answers 503. It is the default of the operation's setting <service>-<kind>-<operation>-bulkhead-queue, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type CLI

type CLI struct {
	// contains filtered or unexported fields
}

CLI is a short command-line command of the product: it runs once, in the CLI profile, and exits with its status. Main runs it when the process's first argument is its name.

func (*CLI) ID

func (n *CLI) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type CLIFunc

type CLIFunc func(ctx context.Context, args []string, std StdioValue) int

CLIFunc is a CLI command's body: its arguments after its name, and the streams; it returns the process exit status (0 success, 64 usage, 70 software — the sysexits a shell script branches on).

type ChangeEvent

type ChangeEvent[E any, S comparable] struct {
	// Key is the entity's key in the store.
	Key string
	// Entity is the entity, as stored after the transition.
	Entity E
	// Event is the transition's name; "create" for [WorkflowService.Start].
	Event string
	// From is the state left; the zero S on creation.
	From S
	// To is the state entered.
	To S
	// Trigger is "create", "event", "timer" or "guard".
	Trigger string
	// Caller is the node that fired it, when code did.
	Caller string
	// At is when it happened.
	At time.Time
}

ChangeEvent describes one transition, for WorkflowService.OnTransition hooks: the entity, the event, and the states it left and entered.

type Classifier added in v0.59.0

type Classifier interface {
	Transient(err error) bool
}

Classifier is an Engine that reads its driver's errors: Transient says whether a failed transaction is one the database asks to run again — on PostgreSQL SQLSTATE 40001 and 40P01, on MySQL errors 1213 and 1205, on SQLite a busy or locked database —, a sibling of Engine. The framework runs such a transaction again, the outermost level only, up to <name>-tx-retries times, waiting <name>-tx-retry-delay grown by 2 with full jitter on the app's clock, each retry a retries attribute on the transaction's span and a retry on the telemetry port. A transaction a Service.Go body opens runs once: the framework cannot know the body may run its function twice. An engine that does not implement it still has the SDK's own reading of SQLSTATE and SQLite result codes.

type Command

type Command[C, R any] struct {
	// contains filtered or unexported fields
}

Command changes something: a typed input, a typed result, and one handler — the one declared with it. Declare it with Service.Command; dispatch it with Command.Dispatch from any building block, which draws a dispatches edge from the caller; give it a route with Command.Expose. A command is internal until it is exposed: how one service has another change something.

A dispatch runs, in this order: the caller's user when the command asks for one (Auth, implied by Command.Allow) — an in-process dispatch carries its caller's and is not authenticated again —, the input's validate tags, the policies (RateLimit, RateLimitPerClient, Timeout, Bulkhead), the key (Command.Key), the transaction, the authorization (Command.Allow, then Command.Authorize), then the handler. A queued command (Queued) is checked at its dispatch, as its caller — its validate tags, its authorization, its key —, and handled by its queue's consumer, the policies around the handling, its key then its transaction around the handler.

A command is its unit of work (ADR 0004): its authorization and its handler run in one transaction (Transact), opened once its key is held, which holds its effects — a publish, a mail, a queued command's dispatch — until the commit. A failed command changes nothing and announces nothing; one dispatched inside another runs in its transaction. NoTransaction opts a command out.

func NewCommand

func NewCommand[C, R any]() *Command[C, R]

NewCommand is a command no service declares yet: Service.Command makes one and declares it, which is how a product gets one.

func (*Command[C, R]) Allow

func (c *Command[C, R]) Allow(policy authz.Policy, action, resource string) *Command[C, R]

Allow makes the command need a permission, as data: the app's policy must let the caller — its user, with the attributes its auth data gives when it implements AttrsProvider — do action to resource. The SDK's authz decides, and closes toward refusal: a role with no grant abstains, and an abstention is refused. Allow implies Auth; given twice, both permissions are needed. The refusal is 403 permission_denied, one sentence whatever the cause.

var PlaceOrder = Service.Command("place-order", placeOrder).Allow(Policy, "place", "order")

func (*Command[C, R]) Authorize

func (c *Command[C, R]) Authorize(fn func(context.Context, C) error) *Command[C, R]

Authorize gives the command a rule that needs the data — the ownership of what the input names, the membership of an organisation: fn sees the input and, through its context, the caller (UserID, AuthData), and returns nil, Forbidden, NotFound — to hide what exists — or an authz.Check of its own. It runs last before the handler, after Command.Allow, under the command's key; its refusal is 403 permission_denied, one sentence whatever the cause, and a NotFound or an Unauthenticated stays what it is. The static analysis reads fn as the command's code: the store it reads is the command's edge.

func (*Command[C, R]) Dispatch

func (c *Command[C, R]) Dispatch(ctx context.Context, in C) (R, error)

Dispatch runs the command for the node ctx runs inside — in a span on the command, its caller's dispatches edge —, through its whole pipeline, and returns its result; a queued command returns once its queue accepted it. The caller's user goes with it.

func (*Command[C, R]) DispatchAt added in v0.50.0

func (c *Command[C, R]) DispatchAt(ctx context.Context, in C, at time.Time) error

DispatchAt queues a dispatch of the queued command that its consumer handles no sooner than at — a reminder, a trial that ends, an order cancelled if still unpaid —, checked now as Command.Dispatch checks it, as its caller: its validate tags, its authorization, its key. It returns once the command's queue accepted it; inside a transaction it is held to the commit as a dispatch is. A dispatch whose key (Command.Key) waits — for later too — or runs is not queued again. An instant already past is now. A command that is not queued refuses it: only a queue holds a dispatch until its instant. It is the SDK's queue.PublishAt.

func (*Command[C, R]) Expose

func (c *Command[C, R]) Expose(route string, opts ...ExposeConfigurer) *Command[C, R]

Expose declares an endpoint that dispatches the command, on route — "METHOD /path/{param}" — with the options an endpoint takes (RateLimitPerClient, MaxBody, Name). It is an endpoint, named after the command — Name for a second exposure —, its request decoded as strictly as any, authenticated when the command asks for a user; the rest — validation, key, authorization — is the command's, so an exposed dispatch and an in-process one are checked alike. It answers 200 with the result, 204 for EmptyValue, 202 for a queued command.

An exposed command says who may run it: a permission (Command.Allow), a rule (Command.Authorize), or, when it declares neither, a marker that it is open on purpose — Anyone, or AnyUser for any signed-in user. The start refuses an exposure that says none of them. A command nobody exposes is internal: the code that needs it dispatches it.

func (*Command) ID

func (n *Command) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*Command[C, R]) Key

func (c *Command[C, R]) Key(fn func(C) string) *Command[C, R]

Key names the entity the command is about: two runs of the command with one key never overlap — the second waits, within its context's deadline —, runs with different keys go at once, and a queued command whose key waits or runs is not queued again until it is handled or dead-lettered. A handler that dispatches its own command with its own key is refused (CodeCommandReentrant), never a deadlock. An input whose key is empty is refused. The key is the span's instance: the Studio filters the runs of one entity.

It is the SDK's lock, in the process: across a restart or between processes, one key can run twice — a handler stays idempotent.

type CommandConfigurer

type CommandConfigurer interface {
	// contains filtered or unexported methods
}

CommandConfigurer configures a command: Queued; the options every operation takes — Auth, AuthOptional, RateLimit, RateLimitPerClient, Timeout, Bulkhead; and, for a queued one, MaxDeliveries and Parallelism.

func NoTransaction

func NoTransaction() CommandConfigurer

NoTransaction runs a command outside a transaction of its own: its writes land as its handler makes them, and its effects leave when made — a failed command may have written or announced something. On the data directory and in memory, where a writing command's transaction takes the writer turn, it lets such commands run side by side. Dispatched inside another command, it still runs in that one's transaction.

func Queued

func Queued() CommandConfigurer

Queued sends a command to the background. Dispatch returns once the command's own queue accepted it — a file queue under the data directory, memory without one —, and a consumer handles it, Parallelism at a time, retried up to MaxDeliveries attempts (5) — half a second after the first failure, the wait doubling after each, never more than a minute —, then dead-lettered; a dispatch that does not decode is dead-lettered at its first failure, since no retry makes it decode. It answers nothing: its result is EmptyValue, and exposed, it answers 202 Accepted.

Delivery is at least once: Queued is the product's promise that the handler tolerates a redelivery, and a Command.Key narrows the duplicates without removing them. The handler runs in the dispatcher's trace, as the user who dispatched it (UserID); what the auth handler said about that user is not queued. A queued command's input is a contract with the messages already queued: add a field, never rename one.

type CommandLineConfigurer

type CommandLineConfigurer interface {
	// contains filtered or unexported methods
}

CommandLineConfigurer tunes a CLI command.

func DefaultCommand

func DefaultCommand() CommandLineConfigurer

DefaultCommand makes the command Main's default: it runs with every argument when the first names no other command — neither one of the product's nor one of Main's (serve, help…) —, and with none when there is none. Main then never answers a usage error. One command of an app may be its default.

IFACE-OPAQUE: CLIOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to CLI.

func FailSafe

func FailSafe() CommandLineConfigurer

FailSafe makes the command's status always 0: a status line, a prompt hook, anything a shell runs on every keystroke must never fail the shell. A non-zero status, a start that fails and a panic are logged instead.

IFACE-OPAQUE: CLIOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to CLI.

type DatabaseConfigurer

type DatabaseConfigurer interface {
	// contains filtered or unexported methods
}

DatabaseConfigurer configures a database: Keeps, Migrations.

func Keeps

func Keeps(things ...Keeper) DatabaseConfigurer

Keeps says what the database keeps: a service's stores, a store, a module's stores. The most precise wins: a store kept by name, then its service, then its module, then the default database — the one declared without Keeps —, then the data directory, then memory. InMemory wins over all of them, on the app as on a store. The framework's data keys never live on SQLite: Keeps(app.Privacy) there keeps its holds and its journal (Privacy).

type DatabaseURLValue

type DatabaseURLValue struct {
	// Driver is the Go driver the engine speaks through: "pgx".
	Driver string
	// Address is where the server listens, host:port; a local socket's
	// path; empty for a file.
	Address string
	// Database is the database on the server, or the file.
	Database string
	// TLS is the TLS mode as the URL writes it — "verify-full", "true" —,
	// empty when the URL leaves it to the driver.
	TLS string
	// Plaintext says the mode the URL writes turns TLS off — sslmode=disable,
	// tls=false —: accepted, for a private network, and drawn "no TLS".
	Plaintext bool
	// Networked says the connection crosses a network, over TCP: outside
	// dev, its URL must write its TLS mode.
	Networked bool
}

DatabaseURLValue is what a database's URL points at, as its engine describes it for the graph, the Studio, the logs and every error: never a user, never a password, never a parameter.

type Delay

type Delay interface {
	time.Duration | *SettingService[time.Duration]
}

Delay is how long a timer transition waits: a duration, or a setting that holds one — read when the delay is needed, so each environment can give its own.

type DeliveryOption

type DeliveryOption interface {
	SubscriptionConfigurer
	CommandConfigurer
}

DeliveryOption configures a queue's deliveries: a subscription's, or a queued command's (MaxDeliveries, Parallelism).

func AutoExtend added in v0.50.0

func AutoExtend(on bool) DeliveryOption

AutoExtend says whether the lease of the message a handler is running is renewed, a third of the lease after it was granted or last renewed, for as long as the handler runs — a subscription's, a watch's, or a queued command's. It is on by default: a slow handler keeps its message instead of seeing it delivered to another while it runs. Off, the handler is stopped a margin before its Lease ends. A renewal the queue refuses cancels the handler's context. A mailer's outbox never renews: the mail spool parks a mail by its own lease.

on is the default of the consumer's setting <service>-<kind>-<name>-auto-extend, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Drain added in v0.50.0

func Drain(d time.Duration) DeliveryOption

Drain is how long the message a handler is running when the app stops may still run before its context is cancelled — a subscription's, a watch's, or a queued command's. A handler that finishes within it is acknowledged; one it cuts short is handed back uncounted, delivered again at the next start, and is no failure. The default is 5 seconds; zero cancels the handler at once, and a negative one stops the start. The app's stop gives each component the longest drain plus its settle (Settle), and tells every consumer at once, so that their drains overlap.

d is the default of the consumer's setting <service>-<kind>-<name>-drain, the kind subscription or command, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Lease added in v0.50.0

func Lease(d time.Duration) DeliveryOption

Lease is how long a delivered message is held by its consumer before the queue hands it to another — a subscription's, a watch's, or a queued command's. The default is 30 seconds; one that is not positive stops the start. With AutoExtend, a handler that runs longer keeps it.

d is the default of the consumer's setting <service>-<kind>-<name>-lease, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func MaxDeliveries

func MaxDeliveries(n int) DeliveryOption

MaxDeliveries is how many attempts a message gets before it is dead-lettered — a subscription's, or a queued command's. The default is 5.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Parallelism

func Parallelism(n int) DeliveryOption

Parallelism is how many messages are handled at once — a subscription's, or a queued command's. The default is 1.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Settle added in v0.50.0

func Settle(d time.Duration) DeliveryOption

Settle bounds each outcome a consumer sends once the app began to stop — an acknowledgement, a nack, a message handed back — and each renewal of a lease (AutoExtend): a queue that stopped answering cannot hold the stop. The default is 5 seconds; one that is not positive stops the start.

d is the default of the consumer's setting <service>-<kind>-<name>-settle, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type DiagnosticsError

type DiagnosticsError struct {
	Diagnostics []model.Diagnostic
	// Missing are the registration packages the program does not import
	// and a declaration needs (ADR 0179 §6), sorted; nil when none is.
	Missing []string
	// Renamed are the old runtime names the start met (ADR 0180 §3, Q12),
	// each written "<old> -> <new>" — "KIT_DATA_DIR -> TODO_DATA_DIR",
	// `shop.Report.Email kit:"personal" -> app:"personal"` —, sorted; nil
	// when none was.
	Renamed []string
}

DiagnosticsError is returned by Start when declarations are wrong. It lists every problem at once, each with its position.

func (*DiagnosticsError) Error

func (e *DiagnosticsError) Error() string

Error lists every problem the start found, one per line.

func (*DiagnosticsError) Unwrap

func (e *DiagnosticsError) Unwrap() error

Unwrap is what the refusal is, when the framework has a code for it: with packages in Missing, an error carrying CodeSubsystemMissing (ADR 0179 §6); with names in Renamed, one carrying CodeRenamed (ADR 0180 §3); both joined when the start found both. errs.HasCode reads either through the refusal; nil when it found neither.

type Dialect

type Dialect string

Dialect is the SQL an Engine speaks: DialectPostgres, DialectMySQL or DialectSQLite, the framework's own names (ADR 0179 Q5) — the graph names the engine by it.

const (
	// DialectPostgres is PostgreSQL.
	DialectPostgres Dialect = "postgres"
	// DialectMySQL is MySQL or MariaDB.
	DialectMySQL Dialect = "mysql"
	// DialectSQLite is SQLite.
	DialectSQLite Dialect = "sqlite"
)

The SQL dialects an Engine speaks: the framework's own names (ADR 0179 Q5), which the store subsystem maps onto the SDK's.

type Edit

type Edit = model.Edit

Edit is one change between two versions of a record, in RFC 6902's words: Op is add, remove or replace, Path the RFC 6901 JSON pointer of the member it changes, From the value it had and To the value it has, as JSON. A secret member says it changed, never what: its From and To are empty.

type EmptyValue

type EmptyValue struct{}

EmptyValue is the request or the response of an endpoint that has none. An endpoint answering EmptyValue replies 204 No Content.

type EndpointConfigurer

type EndpointConfigurer interface {
	ExposeConfigurer
	// contains filtered or unexported methods
}

EndpointConfigurer configures an endpoint; each is an ExposeConfigurer too.

func MaxBody

func MaxBody(bytes int64) EndpointConfigurer

MaxBody bounds the request body the endpoint reads. The default is DefaultMaxBody.

func Name

func Name(name string) EndpointConfigurer

Name overrides the endpoint's name, which otherwise is its handler's function name, or its route when the handler is a function literal.

type EndpointService

type EndpointService[Req, Resp any] struct {
	// contains filtered or unexported fields
}

EndpointService is an HTTP endpoint with a typed request and a typed response: what HTTP itself is about — a webhook a third party calls, say —, the exposure of a command or a query (Command.Expose, Query.Expose), or the implementation of a port (Service.Implement). A business operation is a command or a query, which other code runs with Command.Dispatch and Query.Ask and HTTP reaches through its exposure.

func NewEndpointService

func NewEndpointService[Req, Resp any]() *EndpointService[Req, Resp]

NewEndpointService is an endpoint no service declares yet: Service.Endpoint makes one and declares it, which is how a product gets one.

func (*EndpointService[Req, Resp]) Call

func (e *EndpointService[Req, Resp]) Call(ctx context.Context, req Req) (Resp, error)

Call runs the endpoint in-process, from another building block: the same validation, the same policies, the same handler, and a calls edge in the diagram from the caller to this endpoint. A port calls its implementation so. Services run each other's operations as commands and queries (Command.Dispatch, Query.Ask); calling a handler function directly works, but the product cannot see it.

func (*EndpointService) ID

func (n *EndpointService) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type Engine

type Engine interface {
	// Dialect is the SQL it speaks.
	Dialect() Dialect
	// Describe says what url points at — the driver, host and port, the
	// database, the TLS mode as written — for the graph and the logs: never
	// a user, never a password.
	Describe(url secret.Value) (DatabaseURLValue, error)
	// Open opens the pool url names. Before each new connection it asks
	// current for the URL as it is now.
	Open(url secret.Value, current func(context.Context) (secret.Value, error)) (*stdsql.DB, error)
}

Engine is an SQL engine a database runs on. An engine module returns one — github.com/kitsunium/sdk/framework/connectors/postgres, …/mysql, …/sqlite —; the framework calls it when the database starts. Like an SDK port, it grows by sibling interfaces, never by a method.

type Erasure

type Erasure = model.Erasure

Erasure is what app.Erase did, store by store.

func Erase

func Erase(ctx context.Context, reason string, ids ...string) (Erasure, error)

Erase erases every record whose subject is one of ids, in every store of the app, as its retention would — or deletes it where the store says app.DeleteOnErasure — and folds the stores it wrote, so that what it overwrote leaves their files. A held record is left in place, and the result says so: it lists, per store, what was erased, deleted and held. Telling the recipients (GDPR art. 19) is the product's job; the register lists them. reason is journaled.

What the framework seals at rest, it erases cryptographically too (ADR 0006 §4): a held record is moved under a data key of its own, then the data key of each identity, and every data key the erased records were sealed under, is destroyed — every copy of what they sealed stops opening: a store's former values, a dead letter, a backup. An erasure that failed in a store destroys nothing: its retry does.

type Error

type Error struct {
	// Status is the HTTP status.
	Status int
	// Code is a stable, snake_case identifier a client can branch on.
	Code string
	// Message is wire-safe text for the caller.
	Message string
	// Violations detail an invalid request, one per failed rule.
	Violations []ViolationMessage
	// contains filtered or unexported fields
}

Error is an error a product returns to its callers. Message travels on the wire; the cause attached with Error.Wrap is logged and never sent.

Return one from a handler to choose the HTTP status and the message the caller reads. Any other error becomes a 500 whose body says nothing about it: an error message is the classic place a secret leaks from.

func Conflict

func Conflict(message string) *Error

Conflict reports a request the resource's current state refuses (409).

func Forbidden

func Forbidden(message string) *Error

Forbidden reports a caller who may not do this (403).

func Invalid

func Invalid(message string) *Error

Invalid reports a request the caller must change before retrying (400).

func NewError

func NewError(status int, code, message string) *Error

NewError is an error answered with the HTTP status, carrying the wire code and the message a caller reads.

func NotFound

func NotFound(message string) *Error

NotFound reports that the addressed resource does not exist (404).

func Unauthenticated

func Unauthenticated(message string) *Error

Unauthenticated reports a caller who did not prove who they are (401).

func Unavailable

func Unavailable(message string) *Error

Unavailable reports a transient failure worth retrying later (503).

func (*Error) Error

func (e *Error) Error() string

Error renders the code and the message. It never renders the cause.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the cause, so errors.Is and errors.As see through an Error.

func (*Error) Wrap

func (e *Error) Wrap(cause error) *Error

Wrap attaches the underlying cause, for the logs. It returns a copy.

type ExposeConfigurer

type ExposeConfigurer interface {
	// contains filtered or unexported methods
}

ExposeConfigurer configures an exposure (Command.Expose, Query.Expose): every EndpointConfigurer — Name, MaxBody, RateLimitPerClient… — and the markers that say who may run an operation declaring no rule of its own: Anyone, AnyUser.

func AnyUser

func AnyUser() ExposeConfigurer

AnyUser says that an exposure is open to any signed-in user, on purpose: its operation asks for a user (Auth) and declares no permission and no rule — its handler reads UserID and keeps to what that user owns.

var MyOrders = Service.Query("my-orders", myOrders, app.Auth()).Expose("GET /orders", app.AnyUser())

It adds no authentication of its own: an exposure authenticates as its operation asks, so the operation declares Auth.

func Anyone

func Anyone() ExposeConfigurer

Anyone says that an exposure is open to everyone, on purpose: its operation declares no permission and no rule, and whoever reaches the route may run it — a public form, say. It says so on the exposure's node, where the diagram and the API page read it. An exposure whose operation declares no rule refuses the start unless it says this, or AnyUser.

var Report = Service.Command("report", file).Expose("POST /reports", app.Anyone())

It adds no authentication: an operation that asks for a user (Auth) takes AnyUser instead, and one with AuthOptional still knows the caller who signed in.

type FieldRefValue

type FieldRefValue struct {
	// Store is the store's node ID.
	Store string `json:"store"`
	// Path is the field's JSON pointer in the entity: "/body". A "*"
	// stands for every element of an array or every value of a map:
	// "/replies/*/body".
	Path string `json:"path"`
	// Class is the field's class; "" when it is unclassified.
	Class string `json:"class,omitempty"`
	// Subject is the pointer of the store's subject — for moderated
	// content, its author — or "" when the store has none.
	Subject string `json:"subject,omitempty"`
}

FieldRefValue is one field of one store that carries a mark: where the field is, and what its tag says of it.

type Former

type Former = model.Former

Former is one former value of a field that keeps its history: what it was, until when, and who replaced it.

type Frontend

type Frontend struct {
	// contains filtered or unexported fields
}

Frontend is static assets the product serves: a single page application, a documentation site. Requests the page makes to the product's endpoints are attributed to it in the diagram.

func (*Frontend) ID

func (n *Frontend) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type Job

type Job struct {
	// contains filtered or unexported fields
}

Job is scheduled work: a piece of the daemon's internal loop. It runs on the SDK scheduler, whose decisions about time are documented rather than emergent: a fire that is due while the previous run of the same job is still going is skipped and counted, and a missed deadline is skipped and counted, never caught up.

func (*Job) ID

func (n *Job) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type JobConfigurer added in v0.51.0

type JobConfigurer interface {
	// contains filtered or unexported methods
}

JobConfigurer configures a job (Service.Every, Service.Cron): Overlap, StartingDeadline.

func Overlap added in v0.51.0

func Overlap(p OverlapPolicy) JobConfigurer

Overlap says what a fire of the job does while its previous run is still going: OverlapSkip, the default, skips it and counts it; OverlapAllow runs it beside; OverlapReplace cancels the previous run's context and runs it once that run returned. Any other word stops the start.

p is the default of the job's setting <service>-job-<name>-overlap, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func StartingDeadline added in v0.51.0

func StartingDeadline(d time.Duration) JobConfigurer

StartingDeadline bounds how late a fire of the job may still start: a scheduler that comes to a fire more than d after its instant — a machine that slept, a run that held the slot — does not run it, and counts it missed, as Kubernetes' startingDeadlineSeconds does. The default, zero, is no bound: the latest fire missed runs however late, never more than one. A negative d stops the start.

d is the default of the job's setting <service>-job-<name>-starting-deadline, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type Keeper

type Keeper interface {
	// contains filtered or unexported methods
}

Keeper is what a database keeps: a service — every store it declares —, a store, or a module (ADR 0008) — every store of its services.

type ListenHandler

type ListenHandler func(ctx context.Context, conn *ipc.Conn) error

ListenHandler serves one connection of a Listener until it returns; the connection is closed after it. conn.Peer says who connected, as the kernel says where it can (pkg/v1/proc/ipc).

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener is an inbound port that is not HTTP: a private socket on this machine — a Unix socket in a directory only the product's account can reach, the kernel naming the peer on Linux (ADR 0148) — speaking a versioned contract. It is how a daemon profile serves its clients, and how two process roles of one binary talk (D22): through the contract, never through each other's code.

func NewListener

func NewListener(contract string, handler ListenHandler) *Listener

NewListener is a listener no service declares yet, speaking contract and served by handler: Service.Listen makes one and declares it, which is how a product gets one.

func (*Listener) Dial

func (l *Listener) Dial(ctx context.Context) (*ipc.Conn, error)

Dial connects to the listener as a client of the same account would: the same checks, from the same configuration. A test and a sibling role use it.

func (*Listener) DialIn

func (l *Listener) DialIn(ctx context.Context, product string) (*ipc.Conn, error)

DialIn connects to the listener as run in product, from another process — a client role of the same binary —: the path SocketPathIn computes, the checks Dial makes.

func (*Listener) ID

func (n *Listener) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*Listener) Path

func (l *Listener) Path() string

Path is where the listener listens while the app runs; "" otherwise.

func (*Listener) SocketPathIn

func (l *Listener) SocketPathIn(product string) (string, error)

SocketPathIn is where the listener's socket lies in product — the binary's name for a role of one, else the app's —: its SocketPath, else <runtime dir of product>/<service>-<name>.sock, with SocketPer's key before the extension. A client of another process role computes the daemon's path with it.

type ListenerConfigurer

type ListenerConfigurer interface {
	// contains filtered or unexported methods
}

ListenerConfigurer tunes a listener.

func AllowPeers

func AllowPeers(uids, gids []int) ListenerConfigurer

AllowPeers admits accounts besides the product's own: an on-call group declared at deployment. The kernel checks them where it names the peer; the socket's directory must still let them through, which is the deployment's to arrange.

IFACE-OPAQUE: ListenerOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to Listen.

func SocketPath

func SocketPath(path string) ListenerConfigurer

SocketPath places the listener's socket at path — absolute, at most 103 bytes — instead of <runtime dir of the app>/<service>-<name>.sock.

IFACE-OPAQUE: ListenerOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to Listen.

func SocketPer

func SocketPer(scopes ...ScopeValue) ListenerConfigurer

SocketPer names the listener's socket after the value of scopes in this process — <runtime dir of the app>/<service>-<name>-<key>.sock, the key being ScopeKey's —: one daemon per user, per executable, per configuration directory. A client finds it with SocketPathIn or DialIn, which compute the same key from the same declaration.

IFACE-OPAQUE: ListenerOption is sealed — its one method is unexported — so only this package makes one, and a caller only passes it to Listen.

type LockWaiter

type LockWaiter interface {
	// OpenWaiting opens the pool url names, as Open does, its connections
	// waiting wait for a lock when url writes no wait of its own.
	OpenWaiting(url secret.Value, current func(context.Context) (secret.Value, error), wait time.Duration) (*stdsql.DB, error)
}

LockWaiter is an Engine whose connections wait a bounded time for a lock another connection holds — SQLite's busy timeout —, a sibling of Engine (ADR 0178 C2). The framework opens such an engine's pool with OpenWaiting instead of Open, and gives it a wait under <name>-timeout, so that a lost race on the lock comes back as the engine's refusal before the call's own deadline. An engine that does not implement it keeps its own wait.

type Loop

type Loop struct {
	// contains filtered or unexported fields
}

Loop is a declared loop of the daemon: the framework owns the wait, the product owns the work. Its wake sources — a period, a deadline the product computes, a topic — are data, so the diagram draws exactly what wakes it; the framework runs it on its own goroutine, one run at a time, observes every run, and restarts it after a panic.

func NewLoop

func NewLoop(run func(context.Context, WakeEvent) error) *Loop

NewLoop is a loop no service declares yet, whose runs are run: Service.Loop makes one and declares it, which is how a product gets one.

func (*Loop) ID

func (n *Loop) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*Loop) Nudge

func (l *Loop) Nudge()

Nudge wakes the loop now, as if its deadline had come. The run's reason is model.WakeManual; a nudge also cuts short the backoff after a failure. It does nothing while the loop is not running.

type LoopConfigurer

type LoopConfigurer interface {
	// contains filtered or unexported methods
}

LoopConfigurer configures a declared loop.

func WakeAt

func WakeAt(next func(context.Context) (time.Time, bool)) LoopConfigurer

WakeAt wakes the loop at the time next returns, asked again after every run; false means "no deadline for now". It is how a loop sleeps exactly until its next piece of work — the next reminder due — rather than polling.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func WakeEvery

func WakeEvery(d time.Duration) LoopConfigurer

WakeEvery wakes the loop every d.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func WakeOn

func WakeOn[T any](topic *TopicService[T]) LoopConfigurer

WakeOn wakes the loop whenever a message is published on topic. It is a nudge, not a subscription: the loop is not handed the message and wakes once for a burst — it re-reads the state it works on.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type Mailer

type Mailer struct {
	// contains filtered or unexported fields
}

Mailer is outbound mail: a durable outbox and the transport that empties it. Mailer.Send only queues — it returns once the message is safely in the outbox — and the mailer's own loop hands each message to the transport, retrying a failure and dead-lettering a message after its last attempt.

The transport is the SDK's mail package: SMTP when the mail connector's secret smtp-url is set — <PRODUCT>_SMTP_URL, smtp://user:password@host:587?tls= starttls|implicit|none — and otherwise the capture transport, which keeps every message for the Studio's mailbox and for tests (Mailer.Captured) and delivers nothing. Outside dev, the capture transport is reported as a warning. The URL is read again before every delivery: replacing it where it lives — a new version in the store, a new file behind <PRODUCT>_SMTP_URL_FILE — takes effect at the next mail. The outbox and the transports are the SDK's, which the mail subsystem plugs in (framework/app/mail, ADR 0179 step 6).

func NewMailer

func NewMailer() *Mailer

NewMailer is a mailer no service declares yet, with the default attempts: Service.Mailer makes one and declares it, which is how a product gets one.

func (*Mailer) Captured

func (m *Mailer) Captured() []model.MailMessage

Captured returns the messages the capture transport kept, oldest first: what a test reads a verification link from. It is empty with SMTP. A mail appears once the outbox has delivered it — shortly after Send.

func (*Mailer) ID

func (n *Mailer) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*Mailer) Send

func (m *Mailer) Send(ctx context.Context, msg mail.Message) (string, error)

Send validates msg with the SDK's mail rules — a header carrying a line break, an unusable address, an empty body are refused here, not at delivery — and puts it in the outbox. It returns the message's outbox ID. Inside a transaction (Transact, a command's) the mail waits for the commit — a rollback drops it —, and the ID returned at once is the one the mail keeps. When the outbox lives on the transaction's database — the one that keeps the mailer's service, by a store's rules — the mail is written in the transaction, sealed, just before it commits: it exists if and only if the transaction commits, the mailbox hears of it after the commit, and an outbox that refuses it fails the transaction. Elsewhere it is put in the outbox after the commit.

A message without a sender gets the mailer's (From); without a date, the app's time; without a Message-ID, one made of its outbox ID, which every attempt keeps, so a receiver can tell a retried delivery from a new mail.

type MailerConfigurer

type MailerConfigurer interface {
	// contains filtered or unexported methods
}

MailerConfigurer configures a mailer.

func From

func From(name, addr string) MailerConfigurer

From sets the sender of every message that names none.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func MailAttempts

func MailAttempts(n int) MailerConfigurer

MailAttempts is how many deliveries a message gets before it is dead-lettered. The default is 6, backing off from one second.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func MailMaxAge added in v0.47.0

func MailMaxAge(d time.Duration) MailerConfigurer

MailMaxAge is how long after it was queued a mail may still be retried: an attempt that fails later gives it up, as expired, whatever attempts it had left. It is the default of the mailer's setting <mailer>-max-age, which config/<env>.* and <PRODUCT>_<MAILER>_MAX_AGE override; 0 sets no age. The default is 5 days (RFC 5321 §4.5.4.1).

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OnBounce added in v0.47.0

func OnBounce(fn func(ctx context.Context, b *BounceEvent)) MailerConfigurer

OnBounce replaces what the mailer does with a mail it gives up — by default, it logs it as an error naming why — with fn: mark an address undeliverable, tell an operator, send it another way. fn runs on a goroutine of its own once the mailbox is updated — the outbox goes on meanwhile, and fn may send mail —, with a context on the mailer's node that continues the trace of the Send; the mailer's stop waits for it. A panic in fn is logged and changes nothing else. A nil fn keeps the framework's own.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type Mark

type Mark string

Mark is what a module looks for in the product's fields: app.Personal, app.Special or app.Moderated. There is no mark for public or secret: there is nothing to find in the first, and nothing may watch the second.

const (
	// Personal marks a field that holds data about a person: personal,
	// special, or a record's subject.
	Personal Mark = "personal"
	// Special marks a field of GDPR art. 9(1) or art. 10.
	Special Mark = "special"
	// Moderated marks content others see and a moderator may act on.
	Moderated Mark = "moderated"
)

The marks.

type MigrationExecutor

type MigrationExecutor interface {
	// ExecContext runs a statement that returns no rows.
	ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)
	// QueryContext runs a query that returns rows; the caller closes them.
	QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)
	// QueryRowContext runs a query that returns at most one row.
	QueryRowContext(ctx context.Context, query string, args ...any) *stdsql.Row
}

MigrationExecutor is what a migration's step runs its statements on: a transaction, which a step cannot end.

type MigrationStep

type MigrationStep func(ctx context.Context, ex MigrationExecutor) error

MigrationStep is one direction of a migration, run on ex — the transaction its version row is written in, so that both commit together.

func Statements

func Statements(stmts ...string) MigrationStep

Statements is the MigrationStep that runs stmts in order on its executor, stopping at the first that fails — whose error it returns as the driver said it.

type MigrationValue

type MigrationValue struct {
	// Version orders the migration in its set: non-zero and unique; a UTC
	// timestamp is the convention, so that two branches do not collide.
	Version uint64
	// Name labels it in the version table; required.
	Name string
	// Up applies the change; required.
	Up MigrationStep
	// Down reverses it; required — [Irreversible] says out loud that it
	// cannot be reversed.
	Down MigrationStep
}

MigrationValue is one versioned change of a database's schema and its reversal, the framework's own (ADR 0179 step 10): the shape the SDK's sql runs, which the store subsystem hands it.

app.Migration{
	Version: 20261017120000,
	Name:    "notes",
	Up:      app.Statements("CREATE TABLE notes (id TEXT PRIMARY KEY, body TEXT)"),
	Down:    app.Statements("DROP TABLE notes"),
}

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is a set of services a product mounts as one: NewModule declares it, App.With or Mount mounts it.

func NewModule

func NewModule(name, doc string, parts ...ModuleConfigurer) *Module

NewModule declares a module named name — lower-case letters, digits and dashes, starting with a letter; "app", "kit", "external" and "schema" are the framework's — that doc describes, made of parts: its services, which it adopts, the modules it requires (Requires), and the migrations of the data it keeps outside the framework's stores (Migrations), run on the database that keeps it. Declare it as a package-level variable of the one package a product imports:

var Module = app.NewModule("moderation",
	"Reports and moderation under the DSA.\n\nfr: Signalements et modération selon le DSA.",
	intake.Service, desk.Service, app.Requires(identity.Module))

A service a module lists is its own: what it declared so far is renamed, and what it declares later is born qualified. Listing a service links its package; a service is a module's only, and a module's services take declarations from the module's own Go module only.

func (*Module) Name

func (m *Module) Name() string

Name returns the module's name.

func (*Module) Prefix

func (m *Module) Prefix() string

Prefix is where the app running the module serves its routes — "/<name>/" unless the mount says otherwise — and its default before an app runs it. A module's user interface, built with relative URLs, finds its API there.

type ModuleConfigurer

type ModuleConfigurer interface {
	// contains filtered or unexported methods
}

ModuleConfigurer is what a module is made of: its services, the modules it requires (Requires), and the migrations of the data it keeps outside the framework's stores (Migrations).

func Requires

func Requires(other *Module) ModuleConfigurer

Requires says the module needs other: an app that mounts the module and not other mounts other at its defaults, and starts it first. The Go import this takes makes a cycle impossible. A collaboration a module can do without is a port with a fallback instead (Fallback).

type MountConfigurer

type MountConfigurer interface {
	// contains filtered or unexported methods
}

MountConfigurer configures the mount of a module: Prefix, Bind, Only.

func Only

func Only(envs ...string) MountConfigurer

Only makes a mount hold in the environments envs names alone — the name <PRODUCT>_ENV or Env gives, lower-cased, "production" when neither does:

a := app.NewApp("vigie", identity.Service).
	With(app.Mount(debugbar.Module, app.Only("dev", "staging")))

Elsewhere the mount is as if it had not been given, its Prefix and its Bind with it; another mount of the module without Only still mounts it. Several Only on one mount add up. The start refuses an Only that names no environment, a name that cannot be one — lower-case letters, digits and dashes inside, 63 at most —, and a module a mounted module requires (Requires) that its mount leaves out, rather than mounting it at its defaults behind the product's back.

func Prefix

func Prefix(prefix string) MountConfigurer

Prefix serves a module's routes under prefix — "/trust/" — instead of "/<module>/"; "/" shares the product's route space, where a route both declare is refused as any two are. A prefix starts and ends with '/', and never lies under /_app/, nor under /_kit/ while its probes answer there.

type OTLPBatchValue added in v0.52.0

type OTLPBatchValue struct {
	// Signal is OTLPTraces or OTLPMetrics.
	Signal string
	// Document is the OTLP/JSON export request.
	Document []byte
}

OTLPBatchValue is one batch as the product's own sender receives it (OTLPSender): its signal, OTLPTraces or OTLPMetrics, and its OTLP/JSON document, which a collector takes as it is.

type OTLPFullPolicy added in v0.52.0

type OTLPFullPolicy string

OTLPFullPolicy is what an export's full queue does with a new batch.

const (
	// OTLPDropOldest, the default: the oldest batches not being sent are
	// taken out, counted dropped, until the new one fits.
	OTLPDropOldest OTLPFullPolicy = "drop-oldest"
	// OTLPDropNewest: the new batch is refused, counted dropped.
	OTLPDropNewest OTLPFullPolicy = "drop-newest"
	// OTLPBlock: the span or the snapshot waits for room a few seconds, then
	// is refused, counted dropped.
	OTLPBlock OTLPFullPolicy = "block"
)

What an export's full queue does with a new batch: the setting otlp-when-full spells it.

type OpenTelemetryConfigurer added in v0.52.0

type OpenTelemetryConfigurer interface {
	// contains filtered or unexported methods
}

OpenTelemetryConfigurer configures the app's OTLP export: OTLPEndpoint, OTLPBounds, OTLPWhenFull, OTLPMaxAge, OTLPSendTimeout, OTLPSampleRatio, OTLPDatabase, OTLPSender.

func OTLPBounds added in v0.52.0

func OTLPBounds(maxBatches, maxBytes int) OpenTelemetryConfigurer

OTLPBounds bounds each signal's queue: at most maxBatches batches and maxBytes bytes — the defaults of the settings otlp-max-batches (10000) and otlp-max-bytes (256 MiB). A batch larger than maxBytes on its own is dropped.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPDatabase added in v0.55.0

func OTLPDatabase(name string) OpenTelemetryConfigurer

OTLPDatabase keeps the export's queues on the database the app declares under name — the default of the setting otlp-database —, one table per signal (app_otlp__traces, app_otlp__metrics), made by the framework's own migrations: a backlog then outlives a restart with no data directory — a container with no volume — and on the platforms whose file queue the SDK refuses. It needs framework/app/queue, which the start names when the program does not import it; a name the app does not declare stops the start. The empty default keeps the queues in the data directory, else in memory, and so does a declared database that is not open — the app in memory, or in dev without its URL —, which the log says.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPEndpoint added in v0.52.0

func OTLPEndpoint(url string) OpenTelemetryConfigurer

OTLPEndpoint is the collector's base URL — "http://collector:4318" —, each signal's path (/v1/traces, /v1/metrics) appended to it: the default of the setting otlp-endpoint. An absolute http or https URL with a host; the empty default leaves the export off.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPMaxAge added in v0.52.0

OTLPMaxAge is how long after it was queued a batch may still be sent — the default of the setting otlp-max-age (24h): one older is dropped unsent, counted expired. A collector's Retry-After is waited in full, but never past it.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPSampleRatio added in v0.54.0

func OTLPSampleRatio(ratio float64) OpenTelemetryConfigurer

OTLPSampleRatio is the share of the traces the app starts that the export keeps — the default of the setting otlp-sample-ratio (1, every one): from 0, none, to 1, every one, decided on the trace's identifier, so every service that sees the same trace decides alike. A trace another process started is kept or not as that process decided, whatever the ratio: a span is never cut off from its parent. The metrics the framework derives count every unit of work, kept or not.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPSendTimeout added in v0.52.0

func OTLPSendTimeout(d time.Duration) OpenTelemetryConfigurer

OTLPSendTimeout bounds one attempt to send a batch — the default of the setting otlp-send-timeout (10s, OTEL_EXPORTER_OTLP_TIMEOUT's).

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPSender added in v0.52.0

func OTLPSender(send func(ctx context.Context, batch OTLPBatchValue) error) OpenTelemetryConfigurer

OTLPSender replaces the network: each batch is handed to send instead of being posted to a collector — the product's own sender, a bridge to another transport. The queue, its bounds, its age and its retries stay the framework's. A failure send returns is tried again on the backoff — after the delay it carries when it is a resilience.RetryAfter's —, but for a refusal the caller must fix, an Error of a 4xx other than 408 and 429, which dead-letters the batch. The export is then on whatever otlp-endpoint says. A nil send keeps the network.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func OTLPWhenFull added in v0.52.0

OTLPWhenFull is what a full queue does with a new batch — the default of the setting otlp-when-full: OTLPDropOldest (the default), OTLPDropNewest or OTLPBlock.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type Operation

type Operation[Req, Resp any] interface {
	// contains filtered or unexported methods
}

Operation takes a typed request and answers a typed response: an endpoint, a port — and, with ADR 0005, a command or a query. It is what Fallback and Bind give a port, and what Replace replaces in a test; a request or a response of other types does not compile. Its methods are unexported: only the framework implements it.

Every operation is a node of the graph, and runs in a span of its own through its whole pipeline — authentication, validation, policies, the Studio's mocks — whoever calls it.

type OperationOption

type OperationOption interface {
	EndpointConfigurer
	CommandConfigurer
	QueryConfigurer
}

OperationOption configures an endpoint, a command or a query alike: the authentication it asks for (Auth, AuthOptional) and the policies in front of its handler (RateLimit, RateLimitPerClient, Timeout, Bulkhead).

func Auth

func Auth() OperationOption

Auth makes an endpoint require an authenticated caller: the app's auth handler runs first, and a request without valid credentials is answered 401 before the handler runs. A command or a query that asks for one is refused 401 when its caller carries no user: an in-process dispatch carries its caller's, and is not authenticated again.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func AuthOptional

func AuthOptional() OperationOption

AuthOptional runs the app's auth handler when the request carries credentials, and serves anonymous callers too: UserID tells which.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Bulkhead

func Bulkhead(maxConcurrent int, opts ...BulkheadConfigurer) OperationOption

Bulkhead caps how many requests the handler serves at once; the overflow answers 503 — at once, or, given BulkheadQueue and MaxWait, once the queue is full or the wait ran out. It is the SDK's resilience bulkhead.

The values are the defaults of the operation's settings <service>-<kind>-<operation>-bulkhead, -bulkhead-queue and -bulkhead-wait, which config/<env>.* and <PRODUCT>_<NAME> override; a bulkhead below one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RateLimit

func RateLimit(perSecond float64, burst int, opts ...RateLimitConfigurer) OperationOption

RateLimit puts a token bucket in front of the handler: perSecond tokens a second, up to burst at once. An empty bucket answers 429 at once — with Retry-After, the seconds the bucket needs to earn its next token —, or, given MaxWait, once the token due within it did not come. It is the SDK's resilience rate limiter.

The values are the defaults of the operation's settings <service>-<kind>-<operation>-rate, -burst and -rate-wait, which config/<env>.* and <PRODUCT>_<NAME> override; a rate that is not above zero or a burst below one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RateLimitPerClient

func RateLimitPerClient(perSecond float64, burst int) OperationOption

RateLimitPerClient gives every client of an endpoint a token bucket of its own: perSecond tokens a second, up to burst at once. A client is the authenticated user when there is one, and the client's address otherwise (ClientIP); in-process calls through EndpointService.Call share one bucket. One client emptying its bucket is answered 429, with Retry-After; the others are not — which is what a sign-in endpoint needs, where one shared bucket would let a single client lock everybody out.

Buckets unused for ten minutes are forgotten, and at most 10 000 are kept: the least recently used goes first. It is the SDK's keyed rate limiter.

The values are the defaults of the operation's settings <service>-<kind>-<operation>-client-rate and -client-burst, which config/<env>.* and <PRODUCT>_<NAME> override; a rate that is not above zero or a burst below one stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type OverlapPolicy added in v0.51.0

type OverlapPolicy string

OverlapPolicy is what a job's fire does while the job's previous run is still going: OverlapSkip, OverlapAllow or OverlapReplace. It is the word of the job's setting <service>-job-<name>-overlap.

const (
	// OverlapSkip skips the fire, counted, the previous run going on: the
	// default, since the framework cannot know that two runs of a job may
	// run at once.
	OverlapSkip OverlapPolicy = "skip"
	// OverlapAllow runs the fire beside the previous run.
	OverlapAllow OverlapPolicy = "allow"
	// OverlapReplace cancels the previous run's context and runs the fire
	// once that run returned: the newest fire wins, two runs never overlap.
	OverlapReplace OverlapPolicy = "replace"
)

The overlap policies.

type PasswordConfigurer

type PasswordConfigurer interface {
	// contains filtered or unexported methods
}

PasswordConfigurer configures a password policy: MinLength, NotReused, NotCommon.

func MinLength

func MinLength(n int) PasswordConfigurer

MinLength refuses a password shorter than n characters, counted as NIST counts them: one per Unicode code point. The default is 15, NIST's figure for a password used alone; 8 is the floor NIST allows with a second factor, and a smaller n is refused. There is no composition rule, ever: NIST says SHALL NOT.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func NotCommon

func NotCommon() PasswordConfigurer

NotCommon refuses a password among the ten thousand most common ones — the SDK's password.IsCommon, which compares them without regard to case —: the blocklist NIST SP 800-63B-4 requires a verifier to check, beyond the three thousand OWASP ASVS 5.0 (6.2.4) asks for. Every policy checks it (ADR 0007 §2: on by default once the SDK has the list); the option says so where a policy is declared. Beside NIST's fifteen characters the list has little left to refuse; beside a shorter MinLength, it matters.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func NotReused

func NotReused(n int) PasswordConfigurer

NotReused refuses a password that the current hash, or any of the n former ones, verifies — n from 1 to 100. It is a compliance policy, off by default: PCI DSS asks for it, OWASP ASVS 4.0.3 asks most products not to impose it. The framework keeps the field's last n hashes, so it implies history=n, and a tag whose history is smaller than n is refused.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type PasswordPolicyService

type PasswordPolicyService[T any] struct {
	// contains filtered or unexported fields
}

PasswordPolicyService is the policy of one secret field that holds a password's hash: how long a password is at least, and how many former passwords it refuses. StoreService.Passwords declares it; Set, Change and Verify apply it.

func (*PasswordPolicyService[T]) Change

func (p *PasswordPolicyService[T]) Change(ctx context.Context, key string, current, next []byte) error

Change changes the password of the record under key: it verifies the current password first, as Symfony's UserPassword does — a wrong one is an Invalid error —, then sets next as Set does.

func (*PasswordPolicyService[T]) Set

func (p *PasswordPolicyService[T]) Set(ctx context.Context, key string, password []byte) error

Set sets the password of the record under key — a reset flow, behind a reset token: it checks the policy — the length, the most common passwords, and with NotReused the former passwords —, hashes the password with the SDK's password.Hash and stores the hash; the hash it replaces goes to the field's history. A password the policy refuses is an Invalid error, which says it was used recently, never which one; a missing record is a NotFound.

func (*PasswordPolicyService[T]) Verify

func (p *PasswordPolicyService[T]) Verify(ctx context.Context, key string, password []byte) (bool, error)

Verify reports whether password is the one of the record under key, for a login, with the SDK's constant-time Verify. When the stored hash is weaker than the SDK's policy now (password.NeedsRehash), the framework stores it again, hashed anew: the same password, so no former value. A key with no record — or a record with no password yet — costs what a wrong password costs, and answers false.

type PersonalData

type PersonalData = model.PersonalData

PersonalData is a person's data as app.Export gives it.

func Export

func Export(ctx context.Context, ids ...string) (PersonalData, error)

Export returns every record whose subject is one of ids, in every store of the app: each without its secret members, with the former values its fields keep (ADR 0007) — never a secret field's — and the versions its store keeps of it, without their secret members, and per store what GDPR art. 15(1) asks beside the data — the purpose, the retention and the recipients inside the product. A held record is exported like any other. The result is JSON, structured and machine-readable (art. 20); keeping the month of art. 12(3) is the product's job, and so is deciding who may call it.

A person known under several identities — a user's ID in one store, an e-mail address in another — is found by giving all of them. ctx must run inside the app: an endpoint's, a job's, a loop's.

type PortConfigurer

type PortConfigurer[Req, Resp any] interface {
	// contains filtered or unexported methods
}

PortConfigurer configures a port: Fallback.

func Fallback

func Fallback[Req, Resp any](op Operation[Req, Resp]) PortConfigurer[Req, Resp]

Fallback gives a port an implementation from its own side, which it calls when the app binds nothing else: an endpoint — or, with ADR 0005, a command or a query — of the port's types. A port given here is refused when the app starts.

var Enforcer = Service.Port[Measure, Applied]("enforcer", app.Fallback(QueueAPI))

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type PortPolicyConfigurer added in v0.49.0

type PortPolicyConfigurer interface {
	// contains filtered or unexported methods
}

PortPolicyConfigurer guards the calls a port makes: Retry, Budget, Breaker, Hedge, Idempotent, Throttle and Timeout, given to PortService.Guard.

func Breaker added in v0.49.0

func Breaker(failures int, open time.Duration, opts ...BreakerConfigurer) PortPolicyConfigurer

Breaker stops a port's calls from reaching a dependency that keeps failing: after failures failures of the dependency's in a row — or, given BreakerWindow, a share of a window's calls — it opens, and every call fails at once with the SDK's CircuitOpen (503) for open; then it lets HalfOpenTrials trial calls through, one at a time, which close it or open it again, for longer each time under OpenBackoff. A refusal of the caller's own does not count, nor a caller that left, nor a throttle's refusal. It is the SDK's resilience circuit breaker, on the app's clock, one per port and per start.

Its state is the product's: a readiness check, breaker:<port>, degraded — never unready — while it is not closed; each change of phase logged and put on the telemetry port. An operator opens it until reset with the product's `breaker isolate PORT` and closes it with `breaker reset PORT`, kept in the data directory: a start applies it, and so does a running product, within five seconds.

The values are the defaults of the port's settings <service>-port-<name>-breaker-failures, -breaker-open, -breaker-open-max, -breaker-trials, -breaker-window, -breaker-window-calls and -breaker-window-ratio, which config/<env>.* and <PRODUCT>_<NAME> override; a value the breaker cannot run with stops the start, and so does a breaker the SDK refuses.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Budget added in v0.49.0

func Budget(maxTokens int, tokenRatio float64) PortPolicyConfigurer

Budget sizes the retry budget a port's Retry and Hedge share: a bucket of maxTokens tokens, a failure costing one and a success putting back tokenRatio; retries and duplicates are refused once half the tokens are spent, so a dependency that fails for every caller receives fewer extra calls, not every call times the attempts. A port that retries or hedges without it has 10 tokens and a ratio of 0.1, gRPC's example; a Budget on a port that does neither stops the start. It is the SDK's resilience retry budget, one per port and per start.

The values are the defaults of the port's settings <service>-port-<name>-budget-tokens and -budget-ratio, which config/<env>.* and <PRODUCT>_<NAME> override; fewer than one token, or a ratio below a thousandth, stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Hedge added in v0.49.0

func Hedge(delay time.Duration, inFlight int) PortPolicyConfigurer

Hedge races a port's slow call: an attempt that has not answered after delay is joined by a duplicate, and the first to succeed answers — the tail of a dependency's latency cut, at the price of more calls. At most inFlight duplicates run at once across every call, and the port's retry budget (Budget) refuses them while the dependency fails for many: then the call goes on with the attempts it has. Only a port declared Idempotent may hedge — the start refuses another. It is the SDK's resilience hedge, on the app's clock.

The values are the defaults of the port's settings <service>-port-<name>-hedge-delay, -hedge-in-flight and -hedge-max — the duplicates one call may make, 1 —, which config/<env>.* and <PRODUCT>_<NAME> override; a delay that is not positive, or fewer than one, stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Idempotent added in v0.49.0

func Idempotent() PortPolicyConfigurer

Idempotent declares that the port's operation may run more than once for one call, concurrently, with no harm done: what a Hedge needs, which is refused on a port that does not say so. The graph draws it on the port.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Retry added in v0.49.0

func Retry(maxAttempts int, delay time.Duration, opts ...RetryConfigurer) PortPolicyConfigurer

Retry runs a port's call again when it fails for a reason of the dependency's — what the start answers as a 5xx, a 408 or a 429 —, up to maxAttempts attempts in all, waiting between them a backoff that starts at delay and doubles, with full jitter, up to RetryMaxDelay — 30 seconds unless the code says otherwise. A failure that says how long to wait — a Retry-After, a rate limiter's next token (the SDK's resilience.RetryAfter) — is waited that long instead, held at the longest delay, unless NoRetryAfter. A refusal of the caller's own is not retried, nor an open breaker, a throttle or another retry's exhaustion. It is the SDK's resilience retry, on the app's clock, sharing the port's retry budget (Budget) with its hedge.

Each retry is an event on the port's span, which counts them, and a retry event on the telemetry port.

The values are the defaults of the port's settings <service>-port-<name>-retry-attempts, -retry-delay, -retry-max-delay, -retry-jitter (full, decorrelated or none) and -retry-after (whether a failure's own delay is honoured), which config/<env>.* and <PRODUCT>_<NAME> override; fewer than one attempt, or a delay that is not positive, stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Throttle added in v0.49.0

func Throttle(k float64) PortPolicyConfigurer

Throttle refuses, on the caller's side and without sending them, a share of a port's calls while the dependency refuses more than 1/k of the recent ones — the client-side throttling of Google's SRE book: a call is refused with probability (requests − k × accepts) / (requests + 1), over the last two minutes, with the SDK's Throttled (503). A refusal is a 5xx, a 408 or a 429 of the dependency's; a refusal of the caller's own is an accept. It is the SDK's resilience adaptive throttle, on the app's clock; 2 is the book's k.

k is the default of the port's setting <service>-port-<name>-throttle-k, which config/<env>.* and <PRODUCT>_<NAME> override; a k below 1 stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type PortService

type PortService[Req, Resp any] struct {
	// contains filtered or unexported fields
}

PortService is an operation a service needs and does not implement: a typed request, a typed response, a name. Declare it with Service.Port, and call it with PortService.Call as an endpoint is called: validated, observed, drawn.

What a port calls is decided when the app starts, by one rule the static analysis shares: the app's Bind, else the one Service.Implement among the services the app mounts, else the port's own Fallback. The start refuses a port it cannot bind, with every other problem at once.

A port is how a service reaches what it cannot import — a module its host's content, a service another that imports it — the consumer owning the contract.

func NewPortService

func NewPortService[Req, Resp any]() *PortService[Req, Resp]

NewPortService is a port no service declares yet: Service.Port makes one and declares it, which is how a product gets one.

func (*PortService[Req, Resp]) Call

func (p *PortService[Req, Resp]) Call(ctx context.Context, req Req) (Resp, error)

Call runs what the port is bound to: in a span on the port — the caller's calls edge — then, through the policies PortService.Guard put around it, in the bound operation's own span — one per attempt —, through its whole pipeline, carrying the caller's user as EndpointService.Call does.

func (*PortService[Req, Resp]) Guard added in v0.49.0

func (p *PortService[Req, Resp]) Guard(opts ...PortPolicyConfigurer) *PortService[Req, Resp]

Guard puts policies around the calls the port makes — Retry, Budget, Breaker, Hedge with Idempotent, Throttle, Timeout — and returns the port. Whatever the order they are given in, they compose the outermost first as Retry, Hedge, Breaker, Throttle, Timeout, around the operation the port is bound to; the retry and the hedge share the port's retry budget.

Each declares, in the port's service, the settings that tune it, named <service>-port-<name>-<knob>: the code's value is the default, which config/<env>.* and <PRODUCT>_<NAME> override, and a value a policy cannot run with stops the start. A port is guarded once: a second Guard is refused.

var Quote = Service.Port[QuoteRequest, Price]("quote").
	Guard(app.Timeout(2*time.Second), app.Retry(3, 100*time.Millisecond), app.Breaker(5, 30*time.Second))

func (*PortService) ID

func (n *PortService) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type Query

type Query[Q, R any] struct {
	// contains filtered or unexported fields
}

Query reads and changes nothing: a typed input, a typed result, and one handler — the one declared with it. Declare it with Service.Query; ask it with Query.Ask from any building block, which draws an asks edge from the caller; give it a route with Query.Expose. A query is internal until it is exposed: how one service reads what another keeps. The static analysis warns of a query whose code writes a store, fires a workflow, publishes, sends a mail or dispatches a command.

A question runs, in this order: the caller's user when the query asks for one (Auth, implied by Query.Allow), the input's validate tags, the policies, the authorization (Query.Allow, then Query.Authorize), then the handler, on the caller's goroutine.

func NewQuery

func NewQuery[Q, R any]() *Query[Q, R]

NewQuery is a query no service declares yet: Service.Query makes one and declares it, which is how a product gets one.

func (*Query[Q, R]) Allow

func (q *Query[Q, R]) Allow(policy authz.Policy, action, resource string) *Query[Q, R]

Allow makes the query need a permission, as data: the app's policy must let the caller — its user, with the attributes its auth data gives when it implements AttrsProvider — do action to resource. It implies Auth; given twice, both permissions are needed. The refusal is 403 permission_denied, one sentence whatever the cause.

func (*Query[Q, R]) Ask

func (q *Query[Q, R]) Ask(ctx context.Context, in Q) (R, error)

Ask runs the query for the node ctx runs inside — in a span on the query, its caller's asks edge —, through its whole pipeline, and returns its result. The caller's user goes with it.

func (*Query[Q, R]) Authorize

func (q *Query[Q, R]) Authorize(fn func(context.Context, Q) error) *Query[Q, R]

Authorize gives the query a rule that needs the data: fn sees the input and, through its context, the caller, and returns nil, Forbidden, NotFound — to hide what exists — or an authz.Check of its own. It runs last before the handler, after Query.Allow; its refusal is 403 permission_denied, one sentence whatever the cause, and a NotFound or an Unauthenticated stays what it is.

func (*Query[Q, R]) Expose

func (q *Query[Q, R]) Expose(route string, opts ...ExposeConfigurer) *Query[Q, R]

Expose declares an endpoint that asks the query, on route, with the options an endpoint takes (RateLimitPerClient, MaxBody, Name): named after the query, authenticated when the query asks for a user, the rest of the pipeline the query's. It answers 200 with the result, 204 for EmptyValue.

An exposed query says who may ask it: a permission (Query.Allow), a rule (Query.Authorize), or, when it declares neither, a marker that it is open on purpose — Anyone, or AnyUser for any signed-in user. The start refuses an exposure that says none of them. A query nobody exposes is internal: the code that needs it asks it.

func (*Query) ID

func (n *Query) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type QueryConfigurer

type QueryConfigurer interface {
	// contains filtered or unexported methods
}

QueryConfigurer configures a query: the options every operation takes — Auth, AuthOptional, RateLimit, RateLimitPerClient, Timeout, Bulkhead — and a transaction's — ReadOnly, Serializable, RepeatableRead —, which run its Ask in a transaction opened with them.

type RateLimitConfigurer added in v0.48.0

type RateLimitConfigurer interface {
	// contains filtered or unexported methods
}

RateLimitConfigurer configures a RateLimit.

type RecordsService

type RecordsService struct {
	// contains filtered or unexported fields
}

RecordsService is one store's records without their type. Get returns a record without its secret members; Keys lists the records' keys, to a module that watches the store; EraseFields and Delete take a reason and are refused on a held record. Each call is a span on the store's node and an entry in the privacy journal.

func NewRecordsService

func NewRecordsService(port recordsPort) RecordsService

NewRecordsService is the records of the store port reaches: App.Records and RecordsOf make one for a store the app mounts.

func RecordsOf

func RecordsOf(ctx context.Context, store string) (RecordsService, error)

RecordsOf is App.Records for code the app runs and that does not hold it — a module's watch, its endpoints: the records port of the store whose node ID is store, in the app ctx runs in. Outside a running app it is an Unavailable error.

func Screen(ctx context.Context, w app.Written) error {
	recs, err := app.RecordsOf(ctx, w.Store)
	…
	record, err := recs.Get(ctx, w.Key)

func (RecordsService) Delete

func (r RecordsService) Delete(ctx context.Context, key, reason string) error

Delete removes the record under key; a held record is a Conflict.

func (RecordsService) EraseFields

func (r RecordsService) EraseFields(ctx context.Context, key, reason string, paths ...string) error

EraseFields clears the fields at paths — pointers as FieldRefValue gives them — in the record under key: the final removal of a moderated field. A path that names no classified field is an Invalid error; a held record is a Conflict.

func (RecordsService) Get

Get returns the record under key as JSON, its secret members left out.

func (RecordsService) Keys

Keys yields the key of every record the store holds, in key order, and nothing of a record: what a watch reads to catch up on the writes it may have missed. It is offered only to a module one of whose watches the store feeds — any other caller, code outside a module's node included, is refused with a Forbidden error (CodeRecordsKeys) —, and each listing is one entry in the privacy journal, however many keys it yields. A record in the store's trash is not listed. A failure is yielded once, with an empty key, and ends the listing.

type RetryConfigurer added in v0.49.0

type RetryConfigurer interface {
	// contains filtered or unexported methods
}

RetryConfigurer configures a Retry.

func NoRetryAfter added in v0.49.0

func NoRetryAfter() RetryConfigurer

NoRetryAfter makes a Retry wait its backoff whatever a failure says: a Retry-After, a rate limiter's next token. It is the default of the port's setting <service>-port-<name>-retry-after, false, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RetryMaxDelay added in v0.49.0

func RetryMaxDelay(d time.Duration) RetryConfigurer

RetryMaxDelay is the longest a Retry waits between two attempts: the backoff's cap, and the cap on a delay a failure asks for. It is the default of the port's setting <service>-port-<name>-retry-max-delay; one that is not positive stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type RevisionEvent

type RevisionEvent[T any] struct {
	Number  uint64
	At      time.Time
	By      string
	Command string
	Value   T
}

RevisionEvent is one version of a record a store with Revisions keeps: its number — from 1, never given twice while the record exists —, when the write that made it ran, on the app's clock, zero for a record stored before its store kept revisions; who made it — the caller's UserID, empty for a write with no user —; which command made it — its node ID, empty outside a command —; and the record as it was, its secret members zeroed.

type Routine

type Routine struct {
	// contains filtered or unexported fields
}

Routine is a loop written by hand: a function that runs until its context ends. The framework starts it with the app, cancels its context on shutdown and waits for it, restarts it after an error or a panic — backing off from one second to one minute — and reads its select statements to draw what it waits on.

func (*Routine) ID

func (n *Routine) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type RoutineConfigurer added in v0.51.0

type RoutineConfigurer interface {
	// contains filtered or unexported methods
}

RoutineConfigurer configures a hand-written loop (Service.Go).

type ScopeValue

type ScopeValue struct {
	// contains filtered or unexported fields
}

ScopeValue is one part of a path computed at the start: the user, the executable, a configuration directory. Two processes share a singleton or a socket when every scope gives them the same value.

func PerEnv

func PerEnv(name, fallback string) ScopeValue

PerEnv is the environment variable name, else fallback when it is unset or empty; a leading "~/" in either is the user's home directory. The status line's is PerEnv("CLAUDE_CONFIG_DIR", "~/.claude"): one daemon per Claude configuration.

type Secret

type Secret struct {
	// contains filtered or unexported fields
}

Secret is a secret the product declares: provided by the operator — an SMTP URL, an API token — or generated by the framework — a signing or sealing key — and rotated on its schedule.

func NewSecret

func NewSecret() *Secret

NewSecret is a secret no service declares yet, rotated and kept as by default: Service.Secret makes one and declares it, which is how a product gets one.

func (*Secret) ID

func (n *Secret) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*Secret) Open

func (s *Secret) Open(ctx context.Context, box, aad []byte) ([]byte, error)

Open decrypts a box any kept version sealed, with the aad it was sealed with. A box no kept version opens — forged, altered, or sealed by a version pruned since — is refused with an error errors.Is matches with secret.SealInvalid.

func (*Secret) Present

func (s *Secret) Present(ctx context.Context) (bool, error)

Present reports whether the secret is set now, where it lives: false, and no error, when an Optional secret is given nowhere — which the product answers by leaving off what needs it. An error says its store could not be read. A secret the start required is present, unless the operator took it back since.

if ok, err := OpenAIKey.Present(ctx); err != nil || !ok {
	return Verdict{}, err // no key, no automatic screening
}

func (*Secret) Seal

func (s *Secret) Seal(ctx context.Context, plaintext, aad []byte) ([]byte, error)

Seal encrypts plaintext under the secret's current version, binding aad: data that travels beside the box in the clear and must come back unchanged for the box to open.

func (*Secret) Sign

func (s *Secret) Sign(ctx context.Context, message []byte) ([]byte, error)

Sign signs message with the secret's current version.

func (*Secret) Value

func (s *Secret) Value(ctx context.Context) (secret.Value, error)

Value returns the secret's current version. A provided secret is read where it lives at every call.

func (*Secret) Verify

func (s *Secret) Verify(ctx context.Context, message, signature []byte) error

Verify reports whether a kept version signed message: nil when one did, and an error errors.Is matches with secret.SignatureInvalid when none did.

type SecretConfigurer

type SecretConfigurer interface {
	// contains filtered or unexported methods
}

SecretConfigurer configures a secret.

func Generated

func Generated(bytes int) SecretConfigurer

Generated makes the secret the framework's to make: versions of bytes random bytes — 32 for a key — the first made on the first start, a new one every 30 days, the last 3 kept, unless RotateEvery and KeepVersions say otherwise.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func KeepVersions

func KeepVersions(n int) SecretConfigurer

KeepVersions is how many versions of a generated secret a rotation leaves, the new one included: at least 2, so that what the version it replaces sealed or signed still opens and verifies. Cover the longest-lived box or signature in flight: its lifetime divided by the interval, plus one.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func Optional

func Optional() SecretConfigurer

Optional makes a provided secret one the product can do without — the key of a feature that stays off while nobody gives it. A start where it is set nowhere goes on; Secret.Present says whether it is set now, and a use of it while it is absent — Secret.Value, Seal, Open, Sign or Verify — answers an error errs.HasCode matches with CodeSecretMissing. It is read where it lives at every use, as every provided secret is: given later — `secrets set` in the environment's store, a new content in the file its _FILE variable names —, it is found at its next use. A generated secret is the framework's to make and never absent: Optional with Generated is refused.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

func RotateEvery

func RotateEvery(d time.Duration) SecretConfigurer

RotateEvery is how often the framework makes a new version of a generated secret.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is a bounded context of the product: it owns its building blocks — endpoints, stores, topics, subscriptions, workflows, jobs, frontends — and is the unit an App mounts. Declare one per package, as a package-level variable, and declare its building blocks through its methods:

var Service = app.NewService("todos", "The todo list.")
var Todos = Service.Store("todos", func(t Todo) string { return t.ID })
var _ = Service.Endpoint("GET /todos", List)

Declaring never fails: a mistake — a malformed route, a duplicate name — is recorded with its source position and reported, all at once, when the App starts. The diagram shows it too.

func NewService

func NewService(name, doc string) *Service

NewService declares a service. name must be lower-case letters, digits and dashes, starting with a letter; doc is one sentence saying what it owns.

func (*Service) Activity

func (s *Service) Activity(fn func(context.Context) bool) *ActivityHandler

Activity declares that the service is busy while fn returns true: a daemon whose clients are sessions, not connections, stops once the last session ended rather than once the last connection closed. fn is called on the idle watch's tick, from one goroutine, and must answer quickly; a panic counts as busy, and is logged. The value is kept for the declaration:

var _ = Service.Activity(func(context.Context) bool { return sessions.Open() > 0 })

func (*Service) AuthHandler

func (s *Service) AuthHandler[P, D any](name string, handler func(context.Context, P) (UID, D, error)) *AuthenticatorHandler[P, D]

AuthHandler declares the app's authentication handler. The handler returns the user's ID and data; an empty UID with a nil error means "no credentials", which Auth endpoints answer 401 and AuthOptional endpoints serve anonymously; an error — usually Unauthenticated — refuses the request.

The handler runs only for a request that carries at least one of the credentials' cookies or headers; a request with none is anonymous without asking it.

func (*Service) CLI

func (s *Service) CLI(name, doc string, fn CLIFunc, opts ...CommandLineConfigurer) *CLI

CLI declares a command named name, which Main runs when the process is called "<binary> <name> ...". doc is its one-line help.

func (*Service) Command

func (s *Service) Command[C, R any](name string, handler func(context.Context, C) (R, error), opts ...CommandConfigurer) *Command[C, R]

Command declares a command of the service: an operation named name, run by handler, that changes something. Its node is "<service>/command/<name>".

var PlaceOrder = Service.Command("place-order", placeOrder).
	Allow(Policy, "place", "order").
	Expose("POST /orders")

func (*Service) Cron

func (s *Service) Cron(name, expr string, fn func(context.Context) error, opts ...JobConfigurer) *Job

Cron declares a job that runs on a five-field POSIX cron expression, in the process's local time zone. A fire due while its previous run still goes is skipped, and a late fire runs however late, unless Overlap and StartingDeadline say otherwise.

func (*Service) Endpoint

func (s *Service) Endpoint[Req, Resp any](route string, handler func(context.Context, Req) (Resp, error), opts ...EndpointConfigurer) *EndpointService[Req, Resp]

Endpoint declares an HTTP endpoint on route — "METHOD /path/{param}" — run by handler. The request is decoded from the path, the query, the headers (fields tagged `path:"…"`, `query:"…"`, `header:"…"`) and the JSON body (every other field), then checked against its `validate` tags, then handed to the policies (RateLimit, Timeout, Bulkhead) and the handler. The response is JSON, or 204 No Content when it is EmptyValue.

An endpoint is for what HTTP itself is about — a webhook a third party calls. A business operation is a command or a query, exposed on its route (Command.Expose, Query.Expose), and run in process by the code that needs it (Command.Dispatch, Query.Ask).

The endpoint's name, and so its node ID, is the handler's function name.

func (*Service) Every

func (s *Service) Every(name string, period time.Duration, fn func(context.Context) error, opts ...JobConfigurer) *Job

Every declares a job that runs every period, first one period after the app starts. A fire due while its previous run still goes is skipped, and a late fire runs however late, unless Overlap and StartingDeadline say otherwise.

func (*Service) Go

func (s *Service) Go(name string, run func(context.Context) error, opts ...RoutineConfigurer) *Routine

Go declares a hand-written loop. run must return when ctx ends; returning earlier, with or without an error, restarts it after a backoff — more than ten times within ten minutes, and it is not restarted again: the product is no longer live (GiveUpAfter).

Everything run does is drawn from the loop's node: a store it reads, a topic it publishes on. The framework has no span around it — a run lasts as long as the app — so each of those calls starts a trace of its own.

func (*Service) Implement

func (s *Service) Implement[Req, Resp any](port *PortService[Req, Resp], handler func(context.Context, Req) (Resp, error), opts ...EndpointConfigurer) *EndpointService[Req, Resp]

Implement declares that the service implements port: an endpoint of the service with no route, which only the port reaches, in process, when the app binds nothing else and this is the one implementation among the services it mounts. It runs as every endpoint does — the request's validate tags, the options' policies, the caller's user — and is named like one: its Name, else its handler's name, else, for a function literal, the port's name. A handler whose request or response is not the port's does not compile. A host implements a module's port on its own service: it never adds a node to the module's.

var _ = posts.Service.Implement(desk.Enforcer, Enforce)

func (*Service) Listen

func (s *Service) Listen(name, contract string, handler ListenHandler, opts ...ListenerConfigurer) *Listener

Listen declares a listener named name speaking contract — "<name>/v<major>", "render/v1" — whose connections handler serves, one goroutine each.

func (*Service) Loop

func (s *Service) Loop(name string, run func(context.Context, WakeEvent) error, opts ...LoopConfigurer) *Loop

Loop declares a loop run by kit: run is called once at start, then on every wake.

Runs never overlap: wakes that come while the function runs are gathered into one run after it. A run that fails or panics is counted, and the next one waits a backoff — one second, doubling up to a minute — unless it is a Loop.Nudge. A deadline already past wakes the loop at once, but never sooner than a second after the previous run: a WakeAt that keeps answering the past costs a run a second, not a spinning core. A loop whose runs failed more than ten times within ten minutes stops, and the product is no longer live (GiveUpAfter).

func (*Service) Mailer

func (s *Service) Mailer(name string, opts ...MailerConfigurer) *Mailer

Mailer declares outbound mail.

func (*Service) Name

func (s *Service) Name() string

Name returns the service name, which is also its node ID: qualified with its module's, "moderation.intake", when a module lists it.

func (*Service) Port

func (s *Service) Port[Req, Resp any](name string, opts ...PortConfigurer[Req, Resp]) *PortService[Req, Resp]

Port declares a port of the service: an operation it needs, named name, taking Req and answering Resp, which another service implements (Service.Implement) or the app binds (Bind). Its node is "<service>/port/<name>"; its doc is its declaration's doc comment.

func (*Service) Query

func (s *Service) Query[Q, R any](name string, handler func(context.Context, Q) (R, error), opts ...QueryConfigurer) *Query[Q, R]

Query declares a query of the service: an operation named name, run by handler, that reads and changes nothing. Its node is "<service>/query/<name>".

var MyOrders = Service.Query("my-orders", myOrders, app.Auth()).Expose("GET /orders", app.AnyUser())

func (*Service) Secret

func (s *Service) Secret(name string, opts ...SecretConfigurer) *Secret

Secret declares a secret the product needs.

Without an option it is provided: the operator gives it in the environment — the variable <APP>_<NAME>, TODO_STRIPE_KEY for the secret "stripe-key" of the app "todo", or TODO_STRIPE_KEY_FILE naming a file that holds it, the Docker and Kubernetes way — or in the environment's store, with `the framework secrets set`. It is read at every use, so replacing it where it lives takes effect without a restart, and a start without it fails, naming the variable to set.

With Optional, a start without it goes on: the product asks Secret.Present before it uses the secret.

With Generated, the framework makes it: version 1 on the first start, a new version on its schedule, the previous ones kept so that what they sealed or signed still opens and verifies. It lives in the store the environment keeps — an encrypted file store in dev, <PRODUCT>_SECRETS elsewhere. Its variable pins it: the framework then uses the value as it is and does not rotate it.

name is 1 to 63 lower-case letters, digits and dashes, starting and ending with a letter or a digit — one grammar for a file name and a variable — and unique in the app.

func (*Service) Setting

func (s *Service) Setting[T SettingValue](name string, def T, opts ...SettingConfigurer) *SettingService[T]

Setting declares a setting of the service: name holds def unless the environment says otherwise — the variable <APP>_<NAME>, TODO_BASE_URL for the setting "base-url" of the app "todo", or the key name in the product's configuration files. SettingService.Get reads it.

name follows the secrets' grammar — 1 to 63 lower-case letters, digits and dashes, starting and ending with a letter or a digit — and is unique in the app, among its settings and its secrets: the two share their variables.

func (*Service) Static

func (s *Service) Static(name, prefix string, fsys fs.FS, opts ...StaticConfigurer) *Frontend

Static declares a frontend serving fsys under prefix, which must end with '/'. A path with no file extension that matches no file serves index.html, so client-side routing works; a missing file with an extension — a script, a stylesheet — stays a 404. A directory is never listed. The SDK's static handler serves it; the framework draws it and observes every request.

func (*Service) Store

func (s *Service) Store[T any](name string, key func(T) string, opts ...StoreConfigurer) *StoreService[T]

Store declares a store of entities of type T, keyed by key.

func (*Service) Subscribe

func (s *Service) Subscribe[T any](name string, topic *TopicService[T], handler func(context.Context, T) error, opts ...SubscriptionConfigurer) *SubscriptionWorker[T]

Subscribe declares a subscription of the service to topic. Delivery is at least once: handler must be idempotent, which is the one thing the SDK's queue cannot check and the framework therefore asks you to promise here.

func (*Service) Topic

func (s *Service) Topic[T any](name string) *TopicService[T]

Topic declares a topic carrying messages of type T.

func (*Service) Watch

func (s *Service) Watch(name string, mark Mark, handler func(context.Context, WrittenEvent) error, opts ...SubscriptionConfigurer) *Watch

Watch declares a watch of the service: handler is told of every write the framework confirms on a store whose entity holds a field that carries mark — the product's stores and the other modules', never those of the module the service belongs to (for a service of the product's, never the product's own) unless OwnStores says so. The notice says which store, which record and whether it was deleted; the handler reads the record itself (RecordsOf). Delivery is at least once and in no set order, retried and dead-lettered after MaxDeliveries attempts: handler must be idempotent, and it hears the writes it makes itself on another's store — with OwnStores, never a write its own handler makes. After a stop that was not clean — the process died with the watch running —, its next start queues a notice for every record of every store that feeds it, so that no write a crash took the notice of goes unheard; a deletion whose notice was lost is not among them. The mark that says a watch runs is kept in the data directory: an app without one does not catch up (ADR 0178 C6).

var Content = Service.Watch("content", app.Moderated, Screen)
var Posts = Service.Watch("posts", app.Moderated, Stamp, app.OwnStores())

func (*Service) Workflow

func (s *Service) Workflow[E any, S comparable](name string, store *StoreService[E], state func(*E) *S) *WorkflowService[E, S]

Workflow declares a state machine over the entities of store. state returns a pointer to the entity's state field.

type SettingConfigurer

type SettingConfigurer interface {
	// contains filtered or unexported methods
}

SettingConfigurer configures a setting.

func Required

func Required() SettingConfigurer

Required drops a setting's default: a start without a value for it fails, saying where to give one.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type SettingService

type SettingService[T SettingValue] struct {
	// contains filtered or unexported fields
}

SettingService is a value the environment gives a service, declared with Service.Setting.

func NewSettingService

func NewSettingService[T SettingValue](def T) *SettingService[T]

NewSettingService is a setting no service declares yet, holding def until something sets it: Service.Setting makes one and declares it, which is how a product gets one.

func (*SettingService[T]) Get

func (s *SettingService[T]) Get() T

Get returns the setting's value in the app the service is mounted in — its default until an app has started with it.

func (*SettingService[T]) Name

func (s *SettingService[T]) Name() string

Name is the setting's name, its key in a configuration file — in its module's section, for a module's setting.

type SettingValue

type SettingValue interface {
	string | bool | int | int64 | float64 | time.Duration | []string
}

SettingValue is what a setting may hold.

type StaticConfigurer

type StaticConfigurer interface {
	// contains filtered or unexported methods
}

StaticConfigurer configures a frontend.

func Root

func Root(dir string) StaticConfigurer

Root serves the sub-directory dir of the file system rather than its root — "assets" for a //go:embed assets directive.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type StdioValue

type StdioValue struct {
	// In is the command's standard input.
	In io.Reader
	// Out and Err are its standard output and error.
	Out, Err io.Writer
	// Interactive reports whether In is a terminal.
	Interactive bool
}

StdioValue is where a CLI command reads and writes: the process's own streams under Main, buffers in a test.

type StoreConfigurer

type StoreConfigurer interface {
	// contains filtered or unexported methods
}

StoreConfigurer configures a store.

func Anonymise

func Anonymise[T any](fn func(*T)) StoreConfigurer

Anonymise says what an erased record keeps: fn runs first, and generalises into unclassified fields — a birth date to its year, an address to its region; then the framework clears the rest. The framework cannot prove that what remains identifies no one: by declaring the function, the product asserts it.

func DeleteAfter

func DeleteAfter[T any, D Delay](delay D, since func(T) (time.Time, bool)) StoreConfigurer

DeleteAfter deletes a record delay after the instant since returns.

func DeleteAt

func DeleteAt[T any](at func(T) (time.Time, bool)) StoreConfigurer

DeleteAt deletes a record at the instant at returns.

func DeleteOnErasure

func DeleteOnErasure() StoreConfigurer

DeleteOnErasure makes a person's erasure (app.Erase) delete the store's records instead of clearing them, as sessions and accounts want.

func EraseAfter

func EraseAfter[T any, D Delay](delay D, since func(T) (time.Time, bool)) StoreConfigurer

EraseAfter erases a record's personal data delay after the instant since returns — its closing, say; false means "not yet": an open record is never due. delay is a duration, or a duration setting.

func EraseAt

func EraseAt[T any](at func(T) (time.Time, bool)) StoreConfigurer

EraseAt erases a record's personal data at the instant at returns: one the product computed and stored on the record — a retention in calendar months, one that depends on a tenant.

func HeldUntil

func HeldUntil[T any](until func(T) (time.Time, bool), reason string) StoreConfigurer

HeldUntil holds each record until the instant until returns: the law's own retention, such as an invoice kept ten years (GDPR art. 17(3)(b)). reason is the legal ground, for the register. A held record is exported and updated like any other, and neither erased nor deleted.

func Index

func Index[T any](name string, keys func(T) []string) StoreConfigurer

Index declares an index where an entity may have several keys and a key several entities — the users a task is shared with. StoreService.Find reads it. Empty keys are not indexed.

func Purpose

func Purpose(text string) StoreConfigurer

Purpose says why the store keeps its records (GDPR art. 30(1)(b)): the register lists it, and a person's export carries it.

func ReadModel

func ReadModel() StoreConfigurer

ReadModel marks a store as derived from others (ADR 0005): written by projections — the subscriptions that keep it from the write side's events —, read by queries. It changes nothing of how the store runs; the diagram draws it on its domain's read side, and the static analysis warns of a read model written by anything but a subscription. The framework's topics are queues, not logs: a read model cannot be replayed from its events, and one fed by a topic lags the commands that feed it — a query that must read its own writes reads the write side.

var Summaries = Service.Store("summaries", Summary.Key, app.ReadModel())

func RetentionByProduct

func RetentionByProduct(limits string) StoreConfigurer

RetentionByProduct says the product keeps the store's retention itself: its own code erases and deletes what it no longer needs — a record whose life the product's own rules end, a lifecycle the framework cannot read from the entity. limits says, in the product's words, for how long and how, which the register publishes as the store's time limits (GDPR art. 30(1)(f)); "" says only that the product keeps them.

The framework then runs no retention for the store, and warns of neither a missing retention nor a missing subject. Its classified fields keep every other promise: sealed at rest, never shown, exported with their person — when the store names one — and erased by app.Erase and Store.Erase. It goes with app.Purpose, app.HeldUntil, app.Anonymise and app.DeleteOnErasure; with app.EraseAfter, app.EraseAt, app.DeleteAfter or app.DeleteAt it is refused: a store's retention is the framework's or the product's, never both.

func Revisions

func Revisions(n int) StoreConfigurer

Revisions keeps the n previous versions of each record of the store, n from 1 to 100, as WordPress keeps a post's revisions: every write that changes a record makes a version, numbered from 1 and never renumbered, stamped with when it was made, the caller's user and the command that made it; the oldest beyond n are pruned by the write that makes a newer one, unless a legal hold keeps the record.

var Pages = Service.Store("pages", Page.Key, app.Revisions(20))

revs, err := Pages.Revisions(ctx, id)                            // newest first
edits, err := Pages.Diff(ctx, id, revs[3].Number, revs[0].Number)
page, err := Pages.Restore(ctx, id, revs[3].Number)              // a new version
page, err = Pages.Restore(ctx, id, revs[3].Number, "/title")     // the title only

A store with Revisions(n) holds up to n+1 times its records: in memory with the store in the data directory, in the table <table>___vs on a database, which the framework's migrations create.

func Unique

func Unique[T any](name string, key func(T) string) StoreConfigurer

Unique declares a unique index: no two entities may share a key, and a write that would is refused with a Conflict error. An empty key is not indexed. StoreService.Lookup reads it.

var Accounts = Service.Store("accounts", Account.Key,
	app.Unique("email", func(a Account) string { return a.Email }))

type StoreService

type StoreService[T any] struct {
	// contains filtered or unexported fields
}

StoreService is a typed, keyed collection of entities. Every read returns a copy and every write stores one: an entity is kept as its JSON encoding, so what a handler holds can never alias what the store holds, and the memory and file backends behave identically.

With a data directory (<PRODUCT>_DATA_DIR; ".app/data" in dev) the store persists every write before returning, atomically — a crash never leaves a torn file — and a write costs one entity, whatever the store holds. Without one, it lives in memory. A store a database keeps (Database, Keeps) lives in a table of it, and every call is a round trip. A store is a port (ADR 0004): what it runs on is its engine (store_engine.go) — the SDK's document store, in memory, in files or over SQL —; the framework makes it a node of the graph, observes every call, and speaks for its refusals. Its writes join the transaction their context carries (Transact).

func NewStoreService

func NewStoreService[T any](key func(T) string) *StoreService[T]

NewStoreService is a store no service declares yet, keying its entities with key: Service.Store makes one and declares it, which is how a product gets one.

func (*StoreService[T]) Count

func (s *StoreService[T]) Count(ctx context.Context) (int, error)

Count returns how many entities the store holds.

func (*StoreService[T]) Delete

func (s *StoreService[T]) Delete(ctx context.Context, key string) error

Delete removes the entity with the given key, or returns a NotFound error. On a store with Revisions it moves the entity to the store's trash instead, for <store>-trash — 30 days unless that setting says otherwise, 0 deleting at once —: gone from every read, its unique keys free, its versions kept, StoreService.Untrash brings it back, and the framework purges it once its time is up. When the store seals at rest and the entity was its person's last record, their data key goes with it, as with an erasure — at the commit, in a Transact —, once nothing the framework keeps still needs it: a held record, a record in the trash, or a former value or version sealed under it, keeps the key.

func (*StoreService[T]) Diff

func (s *StoreService[T]) Diff(ctx context.Context, key string, from, to uint64) ([]Edit, error)

Diff returns what changed from the version numbered from to the version numbered to of the record under key, in RFC 6902's words — add, remove, replace, each at a JSON pointer — with both values, in the order they apply: none when the two are the same. A secret member says it changed, never what. Showing a text's changes line by line is the reader's: the Studio does it.

func (*StoreService[T]) Erase

func (s *StoreService[T]) Erase(ctx context.Context, key, reason string) error

Erase erases one record now, as its retention would: its personal, special and secret members and its subject cleared — after the store's Anonymise function has kept what it generalises —, its erased fields stamped, the rest kept. A held record is refused with a Conflict that names the hold, never a placed hold's reason. reason is journaled, for whoever runs the product.

func (*StoreService[T]) Filter

func (s *StoreService[T]) Filter(ctx context.Context, keep func(T) bool) ([]T, error)

Filter returns the entities keep accepts, ordered by key. It reads every entity: an index is the way to avoid it.

func (*StoreService[T]) Find

func (s *StoreService[T]) Find(ctx context.Context, index, key string) ([]T, error)

Find returns the entities indexed under key, ordered by their store key. It reads any index, unique or not; no entity is an empty slice, not an error.

func (*StoreService[T]) Former

func (s *StoreService[T]) Former(ctx context.Context, key, pointer string) ([]Former, error)

Former returns the former values of the field at pointer — its JSON pointer (RFC 6901), "/email" — in the record under key, newest first: each with when it was replaced and by whom. A secret field's former values say when they changed, never what they were: their Value is empty. A field that keeps no former values is an Invalid error, a missing record a NotFound.

func (*StoreService[T]) Get

func (s *StoreService[T]) Get(ctx context.Context, key string) (T, error)

Get returns the entity with the given key, or a NotFound error.

func (*StoreService[T]) Hold

func (s *StoreService[T]) Hold(ctx context.Context, key, reason string) error

Hold places a legal hold on the record under key: neither the retention, nor a person's erasure, nor Delete touches it until StoreService.Release lifts it. reason says why — an authority's request, a case under litigation — and is kept for whoever runs the product, never shown in the Studio. Holding a held record again replaces its reason.

func (*StoreService[T]) Insert

func (s *StoreService[T]) Insert(ctx context.Context, v T) error

Insert stores a new entity; it is a Conflict error when the key exists.

func (*StoreService[T]) List

func (s *StoreService[T]) List(ctx context.Context) ([]T, error)

List returns every entity, ordered by key.

func (*StoreService[T]) Lookup

func (s *StoreService[T]) Lookup(ctx context.Context, index, key string) (T, error)

Lookup returns the entity whose key in the unique index is key, or a NotFound error. Asking a non-unique index is an Invalid error: use StoreService.Find.

func (*StoreService[T]) Passwords

func (s *StoreService[T]) Passwords(field func(*T) *string, opts ...PasswordConfigurer) *PasswordPolicyService[T]

Passwords declares the password policy of one secret field of the store's entity; field names it — the framework calls it on a zero entity to find which. The field then only ever holds the password's hash, a PHC string, or nothing before a first password; a write that puts anything else in it is refused. A field that is not secret refuses the start, as does a second policy on the same field.

func (*StoreService[T]) Put

func (s *StoreService[T]) Put(ctx context.Context, v T) error

Put stores the entity, replacing any entity with the same key.

func (*StoreService[T]) Release

func (s *StoreService[T]) Release(ctx context.Context, key string) error

Release lifts the hold on the record under key: the retention takes it up again, from where its instants say. A record no hold keeps is a NotFound error.

func (*StoreService[T]) Restore

func (s *StoreService[T]) Restore(ctx context.Context, key string, number uint64, fields ...string) (T, error)

Restore writes the version numbered number of the record under key back, as a new version: history is never rewritten. It restores the whole version, or only the fields named by JSON pointer (RFC 6901) — "/title" —, as WordPress restores some fields of a revision. It keeps the record's workflow state, which its workflow owns, and every secret member: restoring an account never brings back an old password past its policy, and naming one of them is an Invalid error. A unique index the restored record breaks refuses it with a Conflict, and nothing changes. It returns the record as restored.

func (*StoreService[T]) Revision

func (s *StoreService[T]) Revision(ctx context.Context, key string, number uint64) (RevisionEvent[T], error)

Revision returns the version numbered number of the record under key, its secret members zeroed: a NotFound when the record keeps none of that number — never made, or pruned.

func (*StoreService[T]) Revisions

func (s *StoreService[T]) Revisions(ctx context.Context, key string) ([]RevisionEvent[T], error)

Revisions returns the versions the store keeps of the record under key, newest first: the record as it is now, then the former ones — n at most, more while a legal hold keeps the record. A secret member is zeroed in each: a former password hash never reaches the product. A missing record is a NotFound, a store that keeps no revisions an Invalid.

func (*StoreService[T]) Trashed

func (s *StoreService[T]) Trashed(ctx context.Context) ([]TombstoneEvent, error)

Trashed returns the records in the store's trash, in key order: each one's key, when the product deleted it, and when the framework purges it. A store without Revisions keeps no trash: an Invalid.

func (*StoreService[T]) Untrash

func (s *StoreService[T]) Untrash(ctx context.Context, key string) (T, error)

Untrash brings the record the product deleted under key back from the store's trash, as it was when deleted — its versions kept, no version made — and returns it. A key with no record in the trash — never deleted, purged, or written again since — is a NotFound; a unique index another record took a key of meanwhile refuses it with a Conflict, and it stays in the trash; a store without Revisions keeps no trash: an Invalid.

func (*StoreService[T]) Update

func (s *StoreService[T]) Update(ctx context.Context, key string, fn func(*T) error) (T, error)

Update applies fn to the entity with the given key and stores the result, atomically with respect to every other write of this store. fn must not change the key. An error from fn leaves the entity untouched.

fn runs while the store's writers wait — on a database, inside the write's transaction, the entity locked —: it may read this store, but must not write to it, nor fire a workflow over it — either would wait for the write fn is part of.

type SubscriptionConfigurer

type SubscriptionConfigurer interface {
	// contains filtered or unexported methods
}

SubscriptionConfigurer configures a subscription.

func OwnStores

func OwnStores() SubscriptionConfigurer

OwnStores lets a watch (Service.Watch) hear the stores of its own module too — the product's own, for a watch of the product's —, which a watch never hears by default. Such a watch never hears the writes its own handler makes, in its handler's context — a handler that writes what it watches, a screening that stamps the record it screened, would otherwise hear itself forever —; a write the handler only causes, later and elsewhere — a queued command's handling, a topic's delivery — is heard. A subscription to a topic refuses it: a topic has no stores.

type SubscriptionWorker

type SubscriptionWorker[T any] struct {
	// contains filtered or unexported fields
}

SubscriptionWorker consumes a topic: its handler runs once per message, at least once, retried on error, and dead-lettered after MaxDeliveries attempts.

func (*SubscriptionWorker) ID

func (n *SubscriptionWorker) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type SupervisionConfigurer added in v0.51.0

type SupervisionConfigurer interface {
	DeliveryOption
	LoopConfigurer
	RoutineConfigurer
}

SupervisionConfigurer configures what the framework restarts after a failure: a subscription's, a watch's or a queued command's consumer, a declared loop, a hand-written loop (GiveUpAfter).

func GiveUpAfter added in v0.51.0

func GiveUpAfter(restarts int, within time.Duration) SupervisionConfigurer

GiveUpAfter bounds how often the framework restarts what fails — a subscription's, a watch's or a queued command's consumer whose queue failed, a hand-written loop (Service.Go) that returned, a declared loop (Service.Loop) whose run failed or panicked —: once it failed more than restarts times within within, it is not restarted again, and the product is no longer live — its liveness check "supervised:<node>" fails, the problem names it — so that whatever watches its liveness — an orchestrator's probe — restarts the process; a product nothing probes keeps running without it, and says so in its log, its problems and its telemetry. It is Erlang's restart intensity, the SDK's lifecycle supervisor's GiveUpAfter.

The default is 10 within 10 minutes: with the restarts' backoff — one second, doubling up to a minute — a component that fails at every start is given up about five minutes after its first failure, and one that fails once a minute or less often is restarted forever. A within of zero never gives up; a negative value stops the start.

restarts and within are the defaults of the settings <service>-<kind>-<name>-give-up-restarts and -give-up-within, the kind subscription, command or loop, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its methods are unexported — so a caller only hands it to the declaration it configures.

type TimeoutConfigurer added in v0.49.0

type TimeoutConfigurer interface {
	OperationOption
	PortPolicyConfigurer
}

TimeoutConfigurer configures an endpoint, a command, a query and a port alike: Timeout.

func Timeout

func Timeout(d time.Duration) TimeoutConfigurer

Timeout cancels the handler's context after d and answers 504. A caller that leaves first is no timeout: its request is recorded as 499, logged as information, and nothing it could read is written. It is the SDK's resilience timeout, on the app's clock.

Given to a port's PortService.Guard, it is a deadline on each attempt of the port's call, the innermost of its policies: an attempt that outruns it fails with the SDK's TimeoutExceeded, which the port's breaker counts and its retry replays.

d is the default of the operation's setting <service>-<kind>-<operation>-timeout — <service>-port-<name>-timeout for a port —, which config/<env>.* and <PRODUCT>_<NAME> override; one that is not positive stops the start.

IFACE-OPAQUE: the option is sealed — its method is unexported — so a caller only hands it to the declaration it configures.

type TombstoneEvent

type TombstoneEvent struct {
	Key     string
	At      time.Time
	PurgeAt time.Time
}

TombstoneEvent is one record in a store's trash: its key, when the product deleted it, on the app's clock, and when the framework purges it — its trash's length after the deletion, or sooner when its retention says so.

type TopicService

type TopicService[T any] struct {
	// contains filtered or unexported fields
}

TopicService is an asynchronous channel of messages of type T. Every subscription receives every message, at least once: a subscription's handler must tolerate a redelivery.

func NewTopicService

func NewTopicService[T any]() *TopicService[T]

NewTopicService is a topic no service declares yet: Service.Topic makes one and declares it, which is how a product gets one.

func (*TopicService) ID

func (n *TopicService) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*TopicService[T]) Once added in v0.50.0

func (t *TopicService[T]) Once(key func(T) string, window time.Duration) *TopicService[T]

Once makes the topic publish a message at most once per key over window: a second TopicService.Publish whose message has the key of one published within the window publishes nothing, in no subscription's queue, and answers nil — a producer that retries a publication it never heard back from adds no copy. The window is counted by each subscription's queue, from its first publication under the key; it is the SDK's queue.PublishOnce. A message whose key is empty is refused. Delivery stays at least once: a handler still tolerates a redelivery.

window is the default of the topic's setting <service>-topic-<name>-once-window, which config/<env>.* and <PRODUCT>_<NAME> override; one that is not positive stops the start. A queued command keeps its own key (Command.Key): a dispatch whose key waits or runs is not queued again, however long ago it was dispatched.

func (*TopicService[T]) Publish

func (t *TopicService[T]) Publish(ctx context.Context, msg T) error

Publish sends msg to every subscription. It returns once every subscription's queue has accepted the message; delivery happens later. Inside a transaction (Transact, a command's) the message is encoded now and queued at the commit: in the transaction, just before it commits, for a subscription whose queue lives on the transaction's database — a queue that refuses it then fails the transaction —, once it committed for the others. A rollback drops it. A topic that publishes once (TopicService.Once) publishes a message whose key it published within its window in no queue.

func (*TopicService[T]) PublishAt added in v0.50.0

func (t *TopicService[T]) PublishAt(ctx context.Context, msg T, at time.Time) error

PublishAt sends msg to every subscription as TopicService.Publish does, but no subscription receives it before at: a reminder, a trial that ends, an order cancelled if still unpaid. It is as durable as any message, held to the commit inside a transaction as a publication is; an instant already past is now. It does not recur — a job does. A topic that publishes once refuses it: the SDK has no publication once for later yet. It is the SDK's queue.PublishAt.

type TransactOption added in v0.59.0

type TransactOption struct {
	// contains filtered or unexported fields
}

TransactOption asks a transaction for an isolation level or for read-only: Serializable, RepeatableRead, ReadOnly. None given is the database's default, as always.

func ReadOnly added in v0.59.0

func ReadOnly() TransactOption

ReadOnly opens the transaction read-only: the database is told it only reads, and a store's write inside it is refused (Invalid, CodeTransactionReadOnly) on every backend, before it reaches one. Given to a query's declaration, it runs the query's Ask in such a transaction — the caller's own, when the Ask runs inside one.

func RepeatableRead added in v0.59.0

func RepeatableRead() TransactOption

RepeatableRead opens the transaction at the REPEATABLE READ isolation level: a row it read reads the same until it ends. On the data directory and in memory it changes nothing.

func Serializable added in v0.59.0

func Serializable() TransactOption

Serializable opens the transaction at the SERIALIZABLE isolation level: it commits only if running it alone would have given the same result, and a concurrent transaction that would break that fails instead — on PostgreSQL with SQLSTATE 40001, which the database asks to run again. On the data directory and in memory, whose transactions already take one writer at a time, it changes nothing.

type UID

type UID string

UID identifies an authenticated user. It is what the auth handler returns and what every endpoint reads with UserID.

func UserID

func UserID(ctx context.Context) (UID, bool)

UserID returns the authenticated caller of the request ctx serves, and whether there is one. An in-process EndpointService.Call carries its caller's user along.

type ViolationMessage

type ViolationMessage struct {
	// Path locates the value: "title", "items[2].zip".
	Path string `json:"path"`
	// Rule names the failed rule: "required", "maxlen".
	Rule string `json:"rule"`
	// Message explains it without echoing the value.
	Message string `json:"message"`
}

ViolationMessage is one validation rule a request failed: where, which rule, and why — never the value.

type WaitConfigurer added in v0.48.0

type WaitConfigurer interface {
	RateLimitConfigurer
	BulkheadConfigurer
}

WaitConfigurer configures a RateLimit and a Bulkhead alike.

func MaxWait added in v0.48.0

func MaxWait(d time.Duration) WaitConfigurer

MaxWait lets a call wait, at most d on the app's clock, instead of being refused at once: given to RateLimit, for a token due within d — the calls after it queue behind it —; given to Bulkhead, for a slot to free, in the queue BulkheadQueue sizes. A call still refused answers 429 or 503 as before; a caller that leaves while it waits leaves the queue. Its default is no wait, and a negative one stops the start. It is the default of the operation's setting <service>-<kind>-<operation>-rate-wait or -bulkhead-wait, which config/<env>.* and <PRODUCT>_<NAME> override.

IFACE-OPAQUE: the option is sealed — its methods are unexported — so a caller only hands it to the declaration it configures.

type WakeEvent

type WakeEvent struct {
	// Reason is model.WakeInterval, WakeDeadline, WakeTopic, WakeStart or
	// WakeManual: WakeManual when a nudge is among the wakes the run
	// gathers — the one wake that cuts a backoff short —, else the first
	// that came.
	Reason string
	// At is when the wake happened, on the app's clock.
	At time.Time
	// Topic is the topic node whose publish woke the loop first, when a
	// topic's did.
	Topic string
	// Reasons are the reasons of every wake the run gathers, each once, in
	// the order they came: Reason is among them.
	Reasons []string
	// Topics are the topic nodes whose publish woke the loop into this run,
	// each once, in the order they came: Topic is the first.
	Topics []string
}

WakeEvent says why a declared loop runs: the reason, the instant, and the topic that woke it when one did. Wakes that come while it runs, or while it backs off after a failure, are gathered into one run: Reasons and Topics say every one of them, and Reason the one that decides the run.

type Watch

type Watch struct {
	// contains filtered or unexported fields
}

Watch is a subscription fed by stores instead of a topic: its handler is told of every write the framework confirms on a store whose entity holds a field with its mark. Service.Watch declares one.

func (*Watch) ID

func (n *Watch) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

type WorkflowService

type WorkflowService[E any, S comparable] struct {
	// contains filtered or unexported fields
}

WorkflowService is a state machine over the entities of one store: the states an entity goes through, and the arrows between them. It is declared as data — which is why the diagram can draw it exactly — and it runs four kinds of transitions:

The SDK's state-machine engine runs it (statemachine), which the workflow subsystem plugs in (framework/app/workflow, ADR 0179 step 5). Its loop never polls: it keeps an agenda of the transitions due, sleeps until the earliest, and a write of the store — which may bring one forward — wakes it. A workflow with nothing due does not run at all. Transitions of one entity run one at a time, of different entities at once.

The entity's state lives in the entity itself (the state function returns a pointer to it), so the store stays the single source of truth; the engine keeps, beside it, when each entity entered its state and the history of its transitions — in the workflow's file under the data directory, so a timer keeps counting across a restart.

func NewWorkflowService

func NewWorkflowService[E any, S comparable](store *StoreService[E], state func(*E) *S) *WorkflowService[E, S]

NewWorkflowService is a workflow no service declares yet, over the entities of store whose state state reads: Service.Workflow makes one and declares it, which is how a product gets one.

func (*WorkflowService[E, S]) After

func (w *WorkflowService[E, S]) After[D Delay](event string, d D, from, to S) *WorkflowService[E, S]

After declares a timer transition: an entity that has spent d in state from moves to state to, fired by the workflow's own loop. d is a duration, or a setting of the service that holds one — which must then be positive.

func (*WorkflowService[E, S]) At

func (w *WorkflowService[E, S]) At(event string, from, to S, instant func(E) (time.Time, bool)) *WorkflowService[E, S]

At declares a timer transition at an instant the entity carries — a due date, a deadline: an entity in state from moves to state to once the time instant returns has come; false means no instant for now. The framework asks again each time the entity is written, and the workflow's own loop sleeps until the earliest instant.

func (*WorkflowService[E, S]) Can

func (w *WorkflowService[E, S]) Can(e E, event string) bool

Can reports whether event may be fired on e.

func (*WorkflowService[E, S]) Census

func (w *WorkflowService[E, S]) Census(ctx context.Context) (map[S]int, error)

Census counts the entities per state; every declared state is present. The engine keeps the count; the call is drawn, as the diagram declares it, as a read of the store.

func (*WorkflowService[E, S]) Fire

func (w *WorkflowService[E, S]) Fire(ctx context.Context, key, event string) (E, error)

Fire moves the entity with the given key along the event transition leaving its current state. It is a Conflict error when no such transition exists, and a NotFound error when the entity does not.

func (*WorkflowService) ID

func (n *WorkflowService) ID() string

ID returns the node's identifier in the product graph: "<service>/<kind>/<name>".

func (*WorkflowService[E, S]) Initial

func (w *WorkflowService[E, S]) Initial(state S) *WorkflowService[E, S]

Initial sets the state a new entity enters with WorkflowService.Start.

func (*WorkflowService[E, S]) On

func (w *WorkflowService[E, S]) On(event string, from, to S) *WorkflowService[E, S]

On declares an event transition: WorkflowService.Fire with this event moves an entity in state from to state to. One event may leave several states, but only one arrow per (event, from).

func (*WorkflowService[E, S]) OnEnter

func (w *WorkflowService[E, S]) OnEnter(state S, fn func(context.Context, *E) error) *WorkflowService[E, S]

OnEnter runs fn when an entity enters state, before it is stored. An error cancels the transition, and so does a panic — reported as CodeWorkflowHookPanic, the entity free for the next transition. fn may change the entity, not its state nor its key (CodeWorkflowHookChange), and must not fire this workflow, which is refused (CodeWorkflowReentrant): transitions of one entity run one at a time.

func (*WorkflowService[E, S]) OnTransition

func (w *WorkflowService[E, S]) OnTransition(fn func(context.Context, ChangeEvent[E, S]) error) *WorkflowService[E, S]

OnTransition runs fn after every transition has been stored, whoever fired it — the natural place to publish it on a topic. An error or a panic is logged and shown by the Studio; the transition stands, and the next hooks still run.

func (*WorkflowService[E, S]) Start

func (w *WorkflowService[E, S]) Start(ctx context.Context, e E) (E, error)

Start enters a new entity into the workflow: it sets the initial state, runs the OnEnter hooks, and inserts the entity in the store.

func (*WorkflowService[E, S]) When

func (w *WorkflowService[E, S]) When(event string, from, to S, guard func(E) bool) *WorkflowService[E, S]

When declares a guard transition: an entity in state from moves to state to as soon as guard holds. The guard sees the entity only: the framework checks it each time the entity is written, never on a clock — a condition that depends on the time is an instant, declared with WorkflowService.At.

type WrittenEvent

type WrittenEvent struct {
	// Store is the store's node ID: "shop/store/reviews".
	Store string `json:"store"`
	// Key is the record's key in the store.
	Key string `json:"key"`
	// Deleted is set when the write deleted the record: there is nothing
	// left to read.
	Deleted bool `json:"deleted,omitempty"`
	// Fields are the store's fields that carry the watch's mark, as
	// [App.Fields] lists them. Which of them the write changed, the notice
	// does not say.
	Fields []FieldRefValue `json:"fields,omitempty"`
}

WrittenEvent is what a watch is told of a write: which store, which record, and whether it was deleted — never a value. The watch reads the record itself (RecordsOf): without its secret members, and journaled.

Source Files

Directories

Path Synopsis
Package jobengine is what framework/app/jobs plugs into kit: the scheduler the framework's jobs run on — the SDK's scheduler, on the app's clock — and the schedules it reads from a job's declaration, a period or a five-field cron expression (ADR 0179 §1, step 7).
Package jobengine is what framework/app/jobs plugs into kit: the scheduler the framework's jobs run on — the SDK's scheduler, on the app's clock — and the schedules it reads from a job's declaration, a period or a five-field cron expression (ADR 0179 §1, step 7).
Package mailengine is what framework/app/mail plugs into kit: the outbox the framework's mailers keep — the SDK's mail spool, on the database that keeps the mailer's service in the queue subsystem's sealed broker, else in the data directory or in memory — and the transports that empty it, the SDK's SMTP transport and its capture transport (ADR 0179 §1, step 6).
Package mailengine is what framework/app/mail plugs into kit: the outbox the framework's mailers keep — the SDK's mail spool, on the database that keeps the mailer's service in the queue subsystem's sealed broker, else in the data directory or in memory — and the transports that empty it, the SDK's SMTP transport and its capture transport (ADR 0179 §1, step 6).
Package otlpengine is what framework/app/otlp plugs into the framework: the export app.OTLP declares — the product's spans and the metrics the framework derives from them, each signal kept in a durable queue of its own (the SDK's app/forward over its data/queue, in the data directory, else in memory) and forwarded to an OpenTelemetry collector over OTLP/HTTP, or to the product's own sender (issue #483, ADR 0185).
Package otlpengine is what framework/app/otlp plugs into the framework: the export app.OTLP declares — the product's spans and the metrics the framework derives from them, each signal kept in a durable queue of its own (the SDK's app/forward over its data/queue, in the data directory, else in memory) and forwarded to an OpenTelemetry collector over OTLP/HTTP, or to the product's own sender (issue #483, ADR 0185).
Package plug is where the framework's opt-in subsystems plug in: the HTTP engine, the frontends' file server and the process sampler of the runtime graph (framework/app/server), the Studio — the tool's variables, its routes and the framework's sentences in the languages after English (framework/app/studio) —, the stores and the databases (framework/app/store), the queues (framework/app/queue), the workflows (framework/app/workflow), the mailers (framework/app/mail), the jobs (framework/app/jobs).
Package plug is where the framework's opt-in subsystems plug in: the HTTP engine, the frontends' file server and the process sampler of the runtime graph (framework/app/server), the Studio — the tool's variables, its routes and the framework's sentences in the languages after English (framework/app/studio) —, the stores and the databases (framework/app/store), the queues (framework/app/queue), the workflows (framework/app/workflow), the mailers (framework/app/mail), the jobs (framework/app/jobs).
Package queueengine is what framework/app/queue plugs into kit: the brokers the framework's consumers' queues run on — the SDK's queue in memory, in the data directory and over SQL, a mailer's outbox on a database sealed in front of the SQL one —, the consumer that delivers them, and the migration of a queue's table (ADR 0179 §1, step 4).
Package queueengine is what framework/app/queue plugs into kit: the brokers the framework's consumers' queues run on — the SDK's queue in memory, in the data directory and over SQL, a mailer's outbox on a database sealed in front of the SQL one —, the consumer that delivers them, and the migration of a queue's table (ADR 0179 §1, step 4).
Package serverengine is what framework/app/server plugs into kit: the HTTP engine an app in the server profile serves on (the SDK's net/server), the frontends' file server (the SDK's net/static), and the process sampler of the runtime graph a serving app answers (the SDK's proc/process, and through it runtime/metrics and the spawn engine).
Package serverengine is what framework/app/server plugs into kit: the HTTP engine an app in the server profile serves on (the SDK's net/server), the frontends' file server (the SDK's net/static), and the process sampler of the runtime graph a serving app answers (the SDK's proc/process, and through it runtime/metrics and the spawn engine).
Package storeengine is what framework/app/store plugs into kit: the engines the framework's stores run on — the SDK's document store in memory, in the data directory and over SQL —, the framework's scope on a database — the SDK's transaction manager, its check and its migrator — and the migrations of the framework's tables (ADR 0179 §1).
Package storeengine is what framework/app/store plugs into kit: the engines the framework's stores run on — the SDK's document store in memory, in the data directory and over SQL —, the framework's scope on a database — the SDK's transaction manager, its check and its migrator — and the migrations of the framework's tables (ADR 0179 §1).
Package studioengine is what framework/app/studio plugs into the framework (ADR 0179 §1, step 8; ADR 0180 §4, step 1): the Studio's routes, their guard and their handlers — the graph, the live events as a Server-Sent Events stream (the SDK's net/sse), traces, source, instances, items, and the dev tools with the profiler (the SDK's profiling) —, the tool's variables the framework dev and the Studio set, and the framework's sentences in the languages after English — the catalogues the Studio shows its reader's language from, compiled by the SDK's i18n (Q6).
Package studioengine is what framework/app/studio plugs into the framework (ADR 0179 §1, step 8; ADR 0180 §4, step 1): the Studio's routes, their guard and their handlers — the graph, the live events as a Server-Sent Events stream (the SDK's net/sse), traces, source, instances, items, and the dev tools with the profiler (the SDK's profiling) —, the tool's variables the framework dev and the Studio set, and the framework's sentences in the languages after English — the catalogues the Studio shows its reader's language from, compiled by the SDK's i18n (Q6).
Package workflowengine is what framework/app/workflow plugs into kit: the engine the framework's workflows run on — the SDK's statemachine, instantiated once, over the framework's entity and the state's spelling (ADR 0179 §1, step 5, Q3) —, its loop of timers and guards, and the conversions between the engine's values and plug's.
Package workflowengine is what framework/app/workflow plugs into kit: the engine the framework's workflows run on — the SDK's statemachine, instantiated once, over the framework's entity and the state's spelling (ADR 0179 §1, step 5, Q3) —, its loop of timers and guards, and the conversions between the engine's values and plug's.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL