kit

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 12 Imported by: 0

README

kit

import "github.com/kitsunium/sdk/framework/kit"

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

Package kit — 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 kit — the App: the services a product mounts, served by one process.

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

Package kit — 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 kit — authentication: the handler that says who calls, and the user it names.

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

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

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

Package kit — the app's options and environments.

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

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

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

Package kit is a framework in which every product is its own diagram.

A product is made of services. A service declares its building blocks as package-level values, through its methods:

var Service = kit.NewService("todos", "The todo list.")

var Todos = Service.Store("todos", func(t Todo) string { return t.ID })

var Changes = Service.Topic[ChangeEvent]("changes")

var Lifecycle = Service.Workflow("lifecycle", Todos, func(t *Todo) *Status { return &t.Status }).
	Initial(Open).
	On("complete", Open, Done).
	After("auto-archive", 2*time.Minute, Done, Archived).
	OnTransition(announce)

var Complete = Service.Command("complete", complete).
	Expose("POST /todos/{id}/complete", kit.Anyone())

func complete(ctx context.Context, in ByID) (Todo, error) {
	return Lifecycle.Fire(ctx, in.ID, "complete")
}

An App mounts services and runs them as one process:

var App = kit.NewApp("todo", todos.Service, activity.Service)

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

The values a product declares are also its description. App.Graph returns the product as a [model.Graph]: every node with the position of its declaration and of the function it runs, the edges that exist by construction, the edges the running product observed — each with its counters — and, in dev, the edges the static analysis found in the code, each with its call sites. In dev the app serves it, live, in the Studio at /_kit/.

Building blocks
  • Service.Command and Service.Query: the product's operations — a command changes something and runs in one transaction, a query reads. Each is internal until it is exposed: the code that needs it runs it with Command.Dispatch or Query.Ask, which is how one service reaches another. Command.Expose and Query.Expose give it a route, which says who may run it: the operation's permission (Command.Allow) or rule (Command.Authorize), or a marker that it is open on purpose — Anyone, AnyUser. The start refuses an exposure that says none.
  • Service.Endpoint: a typed HTTP endpoint, for what HTTP itself is about — a webhook a third party calls. The request is decoded strictly — path, query and header fields by tag, the rest from a JSON body — validated from its `validate` tags, then run through the endpoint's mechanics (RateLimit, RateLimitPerClient, Timeout, Bulkhead). Endpoint.Call runs it in-process; a port calls its implementation (Service.Implement) so.
  • Service.Store: a typed, keyed collection, persisted atomically when the app has a data directory, or in a table of the database that keeps it.
  • Transact: one transaction — what it writes commits together, what it publishes, mails or dispatches leaves at the commit.
  • Service.Topic and Service.Subscribe: asynchronous messages, delivered at least once to every subscription, retried, dead-lettered.
  • Service.Workflow: a state machine over a store — event transitions fired by code, timer and guard transitions fired by the daemon's own loop, hooks on entry and after every transition.
  • Service.Every and Service.Cron: scheduled jobs.
  • Service.Static: a frontend.
  • Service.Listen: an inbound port that is not HTTP — a private socket on this machine speaking a versioned contract (pkg/v1/proc/ipc).
  • Service.CLI: a short command-line command, run once by App.Main.

The app, the composition root, says where the data lives: Database declares a database on an engine module the product's main imports — github.com/kitsunium/sdk/framework/connectors/postgres and its siblings, the only code that imports a driver — and Keeps which stores it keeps. A service never names a database: its handle is its store, which runs on the SDK's document store over SQL there, a table per store.

Declaring never fails: a mistake is recorded with its position and App.Start reports every one at once (DiagnosticsError).

Modules

A module (NewModule) is a Go module's services, which a product mounts as one — App.With at its defaults, Mount with a Prefix or a Bind. What it declares is qualified with its name: its services are "<module>.<service>", its settings and secrets "<module>.<name>", its routes served under its prefix; a module it Requires is mounted with it, and the modules start first.

Processes

An app has a process profile (Profile): a server (the default) serves HTTP; a daemon serves no HTTP, answers on its listeners and may stop when idle (IdleStop) — no connection open and no Service.Activity busy —; a CLI run opens what one command reads, runs it and exits. A command may be the app's default (DefaultCommand: every argument that names no other command is its) and never fail its shell (FailSafe). Singleton keeps one process of an app per machine, SingletonPer one per user, executable or configuration (PerUID, PerExecutable, PerConfigDir, PerEnv), and SocketPer names a listener's socket the same way (SocketPathFor computes it for a client). Stop ends the run of the app a context runs in. A binary (NewBinary) holds process roles — an app each, selected by the leading arguments — which talk only through a declared contract (Binary.Talks).

Running

App.Main understands its commands — serve, graph, healthcheck, config, secrets, migrate — and the environment: KIT_ENV (dev serves the Studio; production is the default), KIT_ADDR or PORT, KIT_DATA_DIR, KIT_ALLOWED_HOSTS, KIT_LOG_LEVEL. The Studio's pages are the kit tool's, not the product's: in dev a server serves the read-only API the Studio reads, and no route that acts on the product exists. The mechanics underneath — lifecycle, health, scheduler, queue, tracing, logging, validation, resilience, identifiers — are the kitsunium SDK's.

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

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

Package kit — exposures: an operation an endpoint serves over HTTP, and who may run it.

Package kit — reading a record's former values.

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

Package kit — secondary indexes of a store.

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

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

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

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

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

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

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

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

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

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

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

Package kit — export: a person's data as kit gives it back.

Package kit — CPU profiles of the running process, for the Studio.

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

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

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

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

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

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

Package kit — revisions: a record keeps its versions, diffed and restored.

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

Package kit — 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 kit — secrets: the values the product declares and the operator provides.

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

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

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

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

Package kit — frontends: static assets the product serves.

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

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

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

Package kit — transactions: what a unit of work writes commits together, and what it sends leaves at its commit.

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

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

Index

Constants

const (
    // EnvDev serves the Studio, analyzes the source and binds loopback. Only
    // `kit dev` or an explicit KIT_ENV=dev turns it on.
    EnvDev string = ikit.EnvDev

    // EnvProduction is the default: no Studio, no source, no analysis.
    EnvProduction string = ikit.EnvProduction
)

const (
    // WireInvalid is WireInvalid.
    WireInvalid string = ikit.WireInvalid

    // WireNotFound is WireNotFound.
    WireNotFound string = ikit.WireNotFound

    // WireConflict is WireConflict.
    WireConflict string = ikit.WireConflict

    // WireUnauth is WireUnauth.
    WireUnauth string = ikit.WireUnauth

    // WireForbidden is WireForbidden.
    WireForbidden string = ikit.WireForbidden

    // WireTooLarge is WireTooLarge.
    WireTooLarge string = ikit.WireTooLarge

    // WireUnsupported is WireUnsupported.
    WireUnsupported string = ikit.WireUnsupported

    // WireRateLimited is WireRateLimited.
    WireRateLimited string = ikit.WireRateLimited

    // WireUnavailable is WireUnavailable.
    WireUnavailable string = ikit.WireUnavailable

    // WireTimeout is WireTimeout.
    WireTimeout string = ikit.WireTimeout

    // WireCanceled is WireCanceled.
    WireCanceled string = ikit.WireCanceled

    // WireInternal is WireInternal.
    WireInternal string = ikit.WireInternal

    // CodeStoreDecode is CodeStoreDecode.
    CodeStoreDecode errs.Code = ikit.CodeStoreDecode

    // CodeStoreEncode is CodeStoreEncode.
    CodeStoreEncode errs.Code = ikit.CodeStoreEncode

    // CodeStorePersist is CodeStorePersist.
    CodeStorePersist errs.Code = ikit.CodeStorePersist

    // CodeStoreLoad is CodeStoreLoad.
    CodeStoreLoad errs.Code = ikit.CodeStoreLoad

    // CodeStoreIndex is CodeStoreIndex.
    CodeStoreIndex errs.Code = ikit.CodeStoreIndex

    // CodeTopicEncode is CodeTopicEncode.
    CodeTopicEncode errs.Code = ikit.CodeTopicEncode

    // CodeTopicPublish is CodeTopicPublish.
    CodeTopicPublish errs.Code = ikit.CodeTopicPublish

    // CodeQueueOpen is CodeQueueOpen.
    CodeQueueOpen errs.Code = ikit.CodeQueueOpen

    // CodeUndecodable is CodeUndecodable.
    CodeUndecodable errs.Code = ikit.CodeUndecodable

    // CodeHandlerPanic is CodeHandlerPanic.
    CodeHandlerPanic errs.Code = ikit.CodeHandlerPanic

    // CodeNotMounted is CodeNotMounted.
    CodeNotMounted errs.Code = ikit.CodeNotMounted

    // CodeWorkflowLoad is CodeWorkflowLoad.
    CodeWorkflowLoad errs.Code = ikit.CodeWorkflowLoad

    // A workflow hook panicked: an OnEnter hook fails its transition with it,
    // an OnTransition hook is reported and the transition stands.
    CodeWorkflowHookPanic errs.Code = ikit.CodeWorkflowHookPanic

    // An OnEnter hook changed the state its entity was entering, or the
    // entity's key: the transition is refused.
    CodeWorkflowHookChange errs.Code = ikit.CodeWorkflowHookChange

    // An OnEnter hook fired its own workflow: refused, where it would wait
    // for itself.
    CodeWorkflowReentrant errs.Code = ikit.CodeWorkflowReentrant

    // CodeAppConfig is CodeAppConfig.
    CodeAppConfig errs.Code = ikit.CodeAppConfig

    // CodeAppRunning is CodeAppRunning.
    CodeAppRunning errs.Code = ikit.CodeAppRunning

    // CodeAppListen is CodeAppListen.
    CodeAppListen errs.Code = ikit.CodeAppListen

    // Mailers.
    CodeMailConfig errs.Code = ikit.CodeMailConfig

    // CodeMailEncode is CodeMailEncode.
    CodeMailEncode errs.Code = ikit.CodeMailEncode

    // CodeMailQueue is CodeMailQueue.
    CodeMailQueue errs.Code = ikit.CodeMailQueue

    // Deprecated: kit 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 = ikit.CodeMailUndecodable

    // CodeMailPanic is CodeMailPanic.
    CodeMailPanic errs.Code = ikit.CodeMailPanic

    // Loops.
    CodeLoopPanic errs.Code = ikit.CodeLoopPanic

    // CodeLoopNotMounted is CodeLoopNotMounted.
    CodeLoopNotMounted errs.Code = ikit.CodeLoopNotMounted

    // CodeLoopStart is CodeLoopStart.
    CodeLoopStart errs.Code = ikit.CodeLoopStart

    // Secrets.
    CodeSecretStore errs.Code = ikit.CodeSecretStore

    // CodeSecretMissing is CodeSecretMissing.
    CodeSecretMissing errs.Code = ikit.CodeSecretMissing

    // CodeSecretRead is CodeSecretRead.
    CodeSecretRead errs.Code = ikit.CodeSecretRead

    // CodeSecretRotate is CodeSecretRotate.
    CodeSecretRotate errs.Code = ikit.CodeSecretRotate

    // Databases, ADR 0004.
    CodeDatabaseOpen errs.Code = ikit.CodeDatabaseOpen

    // CodeDatabaseMigrate is CodeDatabaseMigrate.
    CodeDatabaseMigrate errs.Code = ikit.CodeDatabaseMigrate

    // CodeDatabaseUnavailable is CodeDatabaseUnavailable.
    CodeDatabaseUnavailable errs.Code = ikit.CodeDatabaseUnavailable

    // CodeDatabaseConfig is CodeDatabaseConfig.
    CodeDatabaseConfig errs.Code = ikit.CodeDatabaseConfig

    // CodeTransactionSpan is CodeTransactionSpan.
    CodeTransactionSpan errs.Code = ikit.CodeTransactionSpan

    // Privacy (ADR 0006).
    CodePrivacyKey errs.Code = ikit.CodePrivacyKey

    // CodePrivacyErase is CodePrivacyErase.
    CodePrivacyErase errs.Code = ikit.CodePrivacyErase

    // CodePrivacyJournal is CodePrivacyJournal.
    CodePrivacyJournal errs.Code = ikit.CodePrivacyJournal

    // CodePrivacyData is CodePrivacyData.
    CodePrivacyData errs.Code = ikit.CodePrivacyData

    // History (ADR 0007).
    CodeHistoryLoad errs.Code = ikit.CodeHistoryLoad

    // CodeHistoryWrite is CodeHistoryWrite.
    CodeHistoryWrite errs.Code = ikit.CodeHistoryWrite

    // CodeHistoryRead is CodeHistoryRead.
    CodeHistoryRead errs.Code = ikit.CodeHistoryRead

    // CodePasswordHash is CodePasswordHash.
    CodePasswordHash errs.Code = ikit.CodePasswordHash

    // Commands and queries.
    CodeCommandReentrant errs.Code = ikit.CodeCommandReentrant

    // CodeCommandKey is CodeCommandKey.
    CodeCommandKey errs.Code = ikit.CodeCommandKey

    // CodeCommandQueue is CodeCommandQueue.
    CodeCommandQueue errs.Code = ikit.CodeCommandQueue

    // CodeCommandEncode is CodeCommandEncode.
    CodeCommandEncode errs.Code = ikit.CodeCommandEncode

    // Watches (ADR 0008).
    CodeWatchQueue errs.Code = ikit.CodeWatchQueue

    // Sealing at rest (ADR 0006, step 3).
    CodeSealKey errs.Code = ikit.CodeSealKey

    // CodeSealWrite is CodeSealWrite.
    CodeSealWrite errs.Code = ikit.CodeSealWrite

    // CodeSealOpen is CodeSealOpen.
    CodeSealOpen errs.Code = ikit.CodeSealOpen

    // CodeSealRewrap is CodeSealRewrap.
    CodeSealRewrap errs.Code = ikit.CodeSealRewrap

    // CodeSealShred is CodeSealShred.
    CodeSealShred errs.Code = ikit.CodeSealShred

    // Revisions (ADR 0007 §3).
    CodeRevisionRead errs.Code = ikit.CodeRevisionRead

    // CodeRevisionWrite is CodeRevisionWrite.
    CodeRevisionWrite errs.Code = ikit.CodeRevisionWrite

    // CodeRevisionDecode is CodeRevisionDecode.
    CodeRevisionDecode errs.Code = ikit.CodeRevisionDecode

    // 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 = ikit.CodeJobOverlapped

    // CodeHistoryMoved is CodeHistoryMoved.
    CodeHistoryMoved errs.Code = ikit.CodeHistoryMoved

    // CodeNotSource is CodeNotSource.
    CodeNotSource errs.Code = ikit.CodeNotSource

    // CodePasswordMoved is CodePasswordMoved.
    CodePasswordMoved errs.Code = ikit.CodePasswordMoved

    // CodeNothingToErase is CodeNothingToErase.
    CodeNothingToErase errs.Code = ikit.CodeNothingToErase

    // CodeKeyRekeyed is CodeKeyRekeyed.
    CodeKeyRekeyed errs.Code = ikit.CodeKeyRekeyed

    // CodeMigrateRefused is CodeMigrateRefused.
    CodeMigrateRefused errs.Code = ikit.CodeMigrateRefused

    // CodeCatalogue is CodeCatalogue.
    CodeCatalogue errs.Code = ikit.CodeCatalogue

    // CodeRetentionPanic is CodeRetentionPanic.
    CodeRetentionPanic errs.Code = ikit.CodeRetentionPanic

    // The signals kit ends one of its own steps with and catches itself — a
    // caller never receives one —, typed when the SDK made rule 2 a gate.
    CodeSealErased errs.Code = ikit.CodeSealErased

    // CodeSealKeyMoved is CodeSealKeyMoved.
    CodeSealKeyMoved errs.Code = ikit.CodeSealKeyMoved

    // CodeResealInPlace is CodeResealInPlace.
    CodeResealInPlace errs.Code = ikit.CodeResealInPlace

    // CodeResealMoved is CodeResealMoved.
    CodeResealMoved errs.Code = ikit.CodeResealMoved

    // CodeWorkflowNotInPlace is CodeWorkflowNotInPlace.
    CodeWorkflowNotInPlace errs.Code = ikit.CodeWorkflowNotInPlace

    // CodeTransactionPanic is CodeTransactionPanic.
    CodeTransactionPanic errs.Code = ikit.CodeTransactionPanic
)

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

const CodeCLIFailed errs.Code = ikit.CodeCLIFailed

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.

const CodeProfileRead errs.Code = ikit.CodeProfileRead

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

const CodeScopeUnknown errs.Code = ikit.CodeScopeUnknown

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

const CodeSingletonHeld errs.Code = ikit.CodeSingletonHeld

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

const DefaultMaxBody int64 = ikit.DefaultMaxBody

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 *ikit.PortService[Req, Resp], op ikit.Operation[Req, Resp]) interface {
    AppOption
    MountOption
}

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.

app := kit.NewApp("vigie", desk.Service, posts.Service).With(kit.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.

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

func Catalog

func Catalog() []model.Mechanic

Catalog lists the generic mechanics kit 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 KIT_TRUST_PROXY=on — the last address of X-Forwarded-For, the one the trusted proxy appended. It is "" outside an HTTP request.

func Fallback

func Fallback[Req, Resp any](op ikit.Operation[Req, Resp]) ikit.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", kit.Fallback(QueueAPI))

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

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 {
    StoreOption
    AppOption
}

InMemory keeps data in memory. On an app, it overrides the data directory for every store and queue — what a test wants. On a store, it keeps that store in memory even when the app has a data directory.

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 Migrations

func Migrations(m ...sql.Migration) interface {
    DatabaseOption
    ModulePart
}

Migrations are migrations, values of the SDK's sql.Migration, run when the database starts (<name>-migrate: start) or by `<product> migrate up`, kit'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.

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 kit'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 kit 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 ScopeKey

func ScopeKey(scopes ...Scope) (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. kit 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 Endpoint.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 kit 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 ...Scope) (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) 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 := kit.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 after 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. One a queue refuses after the commit 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. On the data directory and in memory kit 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.

type Activity

Activity 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 Activity = ikit.ActivityHandler

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 kit tool gives one, in dev, through Analyzer.

type AnalyzeFunc = ikit.AnalyzeFunc

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 = ikit.App

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.

type AppOption

AppOption configures an app. Options win over the environment.

type AppOption = ikit.AppConfigurer

func Analyze
func Analyze(on bool) AppOption

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

func Analyzer
func Analyzer(fn AnalyzeFunc) AppOption

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) AppOption

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) AppOption

ConfigFiles gives kit the product's configuration files: config/config.<ext>, which every environment reads, and config/<env>.<ext>, the environment's own — <env> being KIT_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

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

func DataDir
func DataDir(dir string) AppOption

DataDir sets where stores and queues persist, instead of KIT_DATA_DIR.

func Database
func Database(name string, engine Engine, opts ...DatabaseOption) AppOption

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 = kit.NewApp("vigie", intake.Service, desk.Service, audit.Service).With(
	// Keeps every store no other database keeps.
	kit.Database("database", postgres.Engine()),
	// The audit trail apart.
	kit.Database("archive", sqlite.Engine(), kit.Keeps(audit.Service)),
)

name is 1 to 49 lower-case letters, digits and dashes, not starting with "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) 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) AppOption

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) AppOption

Env selects the environment, instead of KIT_ENV.

func IdleStop
func IdleStop(d time.Duration) AppOption

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) AppOption

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

func Logs
func Logs(w io.Writer) AppOption

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

func Mount
func Mount(m *Module, opts ...MountOption) AppOption

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

app := kit.NewApp("vigie", identity.Service).
	With(kit.Mount(moderation.Module, kit.Prefix("/"), kit.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 Profile
func Profile(p string) AppOption

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

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

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(kit.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) AppOption

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[V any](name string, value V) AppOption

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) AppOption

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 ...Scope) AppOption

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) AppOption

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

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

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 KIT_TELEMETRY.

type Authenticator

Authenticator 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 Authenticator[P, D any] = ikit.AuthenticatorHandler[P, D]

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 = kit.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 = ikit.Binary

func NewBinary
func NewBinary(name, doc string) *Binary

NewBinary declares the binary named name.

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 = ikit.CLI

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 = ikit.CLIFunc

type CLIOption

CLIOption tunes a CLI command.

type CLIOption = ikit.CommandLineConfigurer

func DefaultCommand
func DefaultCommand() CLIOption

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() CLIOption

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 Change

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

type Change[E any, S comparable] = ikit.ChangeEvent[E, S]

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 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 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.

ADR 0004's step 2 makes a command its unit of work: its authorization and its handler will run in one transaction, which holds its effects until the commit. Until then, a command's writes and effects are made as the handler makes them.

type Command[C, R any] = ikit.Command[C, R]

type CommandOption

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

type CommandOption = ikit.CommandConfigurer

func NoTransaction
func NoTransaction() CommandOption

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() CommandOption

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 after half a second up to MaxDeliveries attempts (5), then dead-lettered. It answers nothing: its result is Empty, 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 DatabaseOption

DatabaseOption configures a database: Keeps, Migrations.

type DatabaseOption = ikit.DatabaseConfigurer

func Keeps
func Keeps(things ...Keepable) DatabaseOption

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.

type DatabaseURL

DatabaseURL 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 DatabaseURL = ikit.DatabaseURLValue

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 = ikit.Delay

type DeliveryOption

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

type DeliveryOption = ikit.DeliveryOption

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.

type DiagnosticsError

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

type DiagnosticsError = ikit.DiagnosticsError

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 = ikit.Edit

type Empty

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

type Empty = ikit.EmptyValue

type Endpoint

Endpoint is an HTTP endpoint with a typed request and a typed response. Declare it with Service.Endpoint; call it from another service with Endpoint.Call, which is how the diagram sees service-to-service calls.

type Endpoint[Req, Resp any] = ikit.EndpointService[Req, Resp]

type EndpointOption

EndpointOption configures an endpoint.

type EndpointOption = ikit.EndpointConfigurer

func MaxBody
func MaxBody(bytes int64) EndpointOption

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

func Name
func Name(name string) EndpointOption

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 Engine

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

type Engine = ikit.Engine

type Erasure

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

type Erasure = ikit.Erasure

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 kit.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.

Once sealing lands (ADR 0006, step 3), an erasure also moves the held records under keys of their own and destroys the subject's data key, so that it reaches every copy kit sealed.

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 = ikit.Error

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 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).

type ExposeOption

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

type ExposeOption = ikit.ExposeConfigurer

func AnyUser
func AnyUser() ExposeOption

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, kit.Auth()).Expose("GET /orders", kit.AnyUser())

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

func Anyone
func Anyone() ExposeOption

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", kit.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 FieldRef

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

type FieldRef = ikit.FieldRefValue

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 = ikit.Former

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 = ikit.Frontend

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 = ikit.Job

type Keepable

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

type Keepable = ikit.Keeper

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 = ikit.ListenHandler

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 = ikit.Listener

type ListenerOption

ListenerOption tunes a listener.

type ListenerOption = ikit.ListenerConfigurer

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

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) ListenerOption

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 ...Scope) ListenerOption

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 Loop

Loop is a declared loop of the daemon: kit 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; kit runs it on its own goroutine, one run at a time, observes every run, and restarts it after a panic.

type Loop = ikit.Loop

type LoopOption

LoopOption configures a declared loop.

type LoopOption = ikit.LoopConfigurer

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

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) LoopOption

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 *ikit.TopicService[T]) LoopOption

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

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 — KIT_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 KIT_SMTP_URL_FILE — takes effect at the next mail.

type Mailer = ikit.Mailer

type MailerOption

MailerOption configures a mailer.

type MailerOption = ikit.MailerConfigurer

func From
func From(name, addr string) MailerOption

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) MailerOption

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.

type Mark

Mark is what a module looks for in the product's fields: kit.Personal, kit.Special or kit.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 = ikit.Mark

const (
    // Personal marks a field that holds data about a person: personal,
    // special, or a record's subject.
    Personal Mark = ikit.Personal

    // Special marks a field of GDPR art. 9(1) or art. 10.
    Special Mark = ikit.Special

    // Moderated marks content others see and a moderator may act on.
    Moderated Mark = ikit.Moderated
)

type Module

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

type Module = ikit.Module

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

NewModule declares a module named name — lower-case letters, digits and dashes, starting with a letter; "kit", "external" and "schema" are kit'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 kit'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 = kit.NewModule("moderation",
	"Reports and moderation under the DSA.\n\nfr: Signalements et modération selon le DSA.",
	intake.Service, desk.Service, kit.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.

type ModulePart

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

type ModulePart = ikit.ModuleConfigurer

func Requires
func Requires(other *Module) ModulePart

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 MountOption

MountOption configures the mount of a module: Prefix, Bind.

type MountOption = ikit.MountConfigurer

func Prefix
func Prefix(prefix string) MountOption

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 /_kit/.

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 kit 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] = ikit.Operation[Req, Resp]

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 = ikit.OperationOption

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) OperationOption

Bulkhead caps how many requests the handler serves at once; the overflow answers 503. It is the SDK's resilience bulkhead.

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) 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. It is the SDK's resilience rate limiter.

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 Endpoint.Call share one bucket. One client emptying its bucket is answered 429; 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.

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) OperationOption

Timeout cancels the handler's context after d and answers 504. It is the SDK's resilience timeout.

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

type PasswordOption

PasswordOption configures a password policy: MinLength, NotReused.

type PasswordOption = ikit.PasswordConfigurer

func MinLength
func MinLength(n int) PasswordOption

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() PasswordOption

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.

func NotReused
func NotReused(n int) PasswordOption

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. kit 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 PasswordPolicy

PasswordPolicy 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. Store.Passwords declares it; Set, Change and Verify apply it.

type PasswordPolicy[T any] = ikit.PasswordPolicyService[T]

type PersonalData

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

type PersonalData = ikit.PersonalData

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 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 Port

Port 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 Port.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 Port[Req, Resp any] = ikit.PortService[Req, Resp]

type PortOption

PortOption configures a port: Fallback.

type PortOption[Req, Resp any] = ikit.PortConfigurer[Req, Resp]

type Principal

Principal 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...)} }

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

type Principal = ikit.AttrsProvider

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. 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] = ikit.Query[Q, R]

type QueryOption

QueryOption configures a query: the options every operation takes — Auth, AuthOptional, RateLimit, RateLimitPerClient, Timeout, Bulkhead.

type QueryOption = ikit.QueryConfigurer

type Records

Records is one store's records without their type. Get returns a record without its secret members; 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 Records = ikit.RecordsService

func RecordsOf
func RecordsOf(ctx context.Context, store string) (Records, 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 kit.Written) error {
	recs, err := kit.RecordsOf(ctx, w.Store)
	…
	record, err := recs.Get(ctx, w.Key)

type Revision

Revision 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 Revision[T any] = ikit.RevisionEvent[T]

type Routine

Routine is a loop written by hand: a function that runs until its context ends. kit 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 = ikit.Routine

type Scope

Scope 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 Scope = ikit.ScopeValue

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

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

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

func PerEnv
func PerEnv(name, fallback string) Scope

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

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

type Secret = ikit.Secret

type SecretOption

SecretOption configures a secret.

type SecretOption = ikit.SecretConfigurer

func Generated
func Generated(bytes int) SecretOption

Generated makes the secret kit'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) SecretOption

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() SecretOption

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 kit's to make and never absent: Optional with Generated is refused.

func RotateEvery
func RotateEvery(d time.Duration) SecretOption

RotateEvery is how often kit 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

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 = kit.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 = ikit.Service

Privacy is kit's own service: the legal holds and the privacy journal of an app that keeps personal data. kit mounts a copy of it — named kit.privacy, a name no service of a product can take — in every app whose stores keep personal data or declare a retention, before the product's services. Its stores stay in the data directory unless the product places them (ADR 0004). A product never mounts it itself.

var Privacy *Service = ikit.Privacy

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.

type Setting

Setting is a value the environment gives a service, declared with Service.Setting.

type Setting[T SettingValue] = ikit.SettingService[T]

type SettingOption

SettingOption configures a setting.

type SettingOption = ikit.SettingConfigurer

func Required
func Required() SettingOption

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 SettingValue

SettingValue is what a setting may hold.

type SettingValue = ikit.SettingValue

type StaticOption

StaticOption configures a frontend.

type StaticOption = ikit.StaticConfigurer

func Root
func Root(dir string) StaticOption

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 Stdio

Stdio is where a CLI command reads and writes: the process's own streams under Main, buffers in a test.

type Stdio = ikit.StdioValue

type Store

Store 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 (KIT_DATA_DIR; ".kit/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 is a port (ADR 0004): what it runs on is its engine (store_engine.go), today the SDK's document store (docstore); kit makes it a node of the graph, observes every call, and speaks for its refusals.

type Store[T any] = ikit.StoreService[T]

type StoreOption

StoreOption configures a store.

type StoreOption = ikit.StoreConfigurer

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

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 kit clears the rest. kit 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)) StoreOption

DeleteAfter deletes a record delay after the instant since returns.

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

DeleteAt deletes a record at the instant at returns.

func DeleteOnErasure
func DeleteOnErasure() StoreOption

DeleteOnErasure makes a person's erasure (kit.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)) StoreOption

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)) StoreOption

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) StoreOption

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) StoreOption

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

func Purpose
func Purpose(text string) StoreOption

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() StoreOption

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. kit'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, kit.ReadModel())

func RetentionByProduct
func RetentionByProduct(limits string) StoreOption

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 kit 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.

kit 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 kit.Erase and Store.Erase. It goes with kit.Purpose, kit.HeldUntil, kit.Anonymise and kit.DeleteOnErasure; with kit.EraseAfter, kit.EraseAt, kit.DeleteAfter or kit.DeleteAt it is refused: a store's retention is kit's or the product's, never both.

func Revisions
func Revisions(n int) StoreOption

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, kit.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 kit's migrations create.

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

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. Store.Lookup reads it.

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

type Subscription

Subscription consumes a topic: its handler runs once per message, at least once, retried on error, and dead-lettered after MaxDeliveries attempts.

type Subscription[T any] = ikit.SubscriptionWorker[T]

type SubscriptionOption

SubscriptionOption configures a subscription.

type SubscriptionOption = ikit.SubscriptionConfigurer

func OwnStores
func OwnStores() SubscriptionOption

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 Topic

Topic 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 Topic[T any] = ikit.TopicService[T]

type UID

UID identifies an authenticated user. It is what the auth handler returns and what every endpoint reads with UserID.

type UID = ikit.UID

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 Endpoint.Call carries its caller's user along.

type Violation

Violation is one validation rule a request failed: where, which rule, and why — never the value.

type Violation = ikit.ViolationMessage

type Wake

Wake says why a declared loop runs: the reason, the instant, and the topic that woke it when one did.

type Wake = ikit.WakeEvent

type Watch

Watch is a subscription fed by stores instead of a topic: its handler is told of every write kit confirms on a store whose entity holds a field with its mark. Service.Watch declares one.

type Watch = ikit.Watch

type Workflow

Workflow 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:

  • event transitions (Workflow.On), fired by code with Workflow.Fire;
  • timer transitions (Workflow.After), fired by the workflow's own loop once an entity has spent a duration in a state;
  • timer transitions at an instant the entity carries (Workflow.At) — a due date, a deadline — fired by the same loop when it comes;
  • guard transitions (Workflow.When), fired by the same loop as soon as a condition on the entity holds.

The SDK's state-machine engine runs it (statemachine). 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 Workflow[E any, S comparable] = ikit.WorkflowService[E, S]

type Written

Written 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 Written = ikit.WrittenEvent

Generated by gomarkdoc

Documentation

Overview

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

Package kit — 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 kit — the App: the services a product mounts, served by one process.

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

Package kit — 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 kit — authentication: the handler that says who calls, and the user it names.

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

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

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

Package kit — the app's options and environments.

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

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

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

Package kit is a framework in which every product is its own diagram.

A product is made of services. A service declares its building blocks as package-level values, through its methods:

var Service = kit.NewService("todos", "The todo list.")

var Todos = Service.Store("todos", func(t Todo) string { return t.ID })

var Changes = Service.Topic[ChangeEvent]("changes")

var Lifecycle = Service.Workflow("lifecycle", Todos, func(t *Todo) *Status { return &t.Status }).
	Initial(Open).
	On("complete", Open, Done).
	After("auto-archive", 2*time.Minute, Done, Archived).
	OnTransition(announce)

var Complete = Service.Command("complete", complete).
	Expose("POST /todos/{id}/complete", kit.Anyone())

func complete(ctx context.Context, in ByID) (Todo, error) {
	return Lifecycle.Fire(ctx, in.ID, "complete")
}

An App mounts services and runs them as one process:

var App = kit.NewApp("todo", todos.Service, activity.Service)

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

The diagram

The values a product declares are also its description. App.Graph returns the product as a model.Graph: every node with the position of its declaration and of the function it runs, the edges that exist by construction, the edges the running product observed — each with its counters — and, in dev, the edges the static analysis found in the code, each with its call sites. In dev the app serves it, live, in the Studio at /_kit/.

Building blocks

  • Service.Command and Service.Query: the product's operations — a command changes something and runs in one transaction, a query reads. Each is internal until it is exposed: the code that needs it runs it with Command.Dispatch or Query.Ask, which is how one service reaches another. Command.Expose and Query.Expose give it a route, which says who may run it: the operation's permission (Command.Allow) or rule (Command.Authorize), or a marker that it is open on purpose — Anyone, AnyUser. The start refuses an exposure that says none.
  • Service.Endpoint: a typed HTTP endpoint, for what HTTP itself is about — a webhook a third party calls. The request is decoded strictly — path, query and header fields by tag, the rest from a JSON body — validated from its `validate` tags, then run through the endpoint's mechanics (RateLimit, RateLimitPerClient, Timeout, Bulkhead). Endpoint.Call runs it in-process; a port calls its implementation (Service.Implement) so.
  • Service.Store: a typed, keyed collection, persisted atomically when the app has a data directory, or in a table of the database that keeps it.
  • Transact: one transaction — what it writes commits together, what it publishes, mails or dispatches leaves at the commit.
  • Service.Topic and Service.Subscribe: asynchronous messages, delivered at least once to every subscription, retried, dead-lettered.
  • Service.Workflow: a state machine over a store — event transitions fired by code, timer and guard transitions fired by the daemon's own loop, hooks on entry and after every transition.
  • Service.Every and Service.Cron: scheduled jobs.
  • Service.Static: a frontend.
  • Service.Listen: an inbound port that is not HTTP — a private socket on this machine speaking a versioned contract (pkg/v1/proc/ipc).
  • Service.CLI: a short command-line command, run once by App.Main.

The app, the composition root, says where the data lives: Database declares a database on an engine module the product's main imports — github.com/kitsunium/sdk/framework/connectors/postgres and its siblings, the only code that imports a driver — and Keeps which stores it keeps. A service never names a database: its handle is its store, which runs on the SDK's document store over SQL there, a table per store.

Declaring never fails: a mistake is recorded with its position and App.Start reports every one at once (DiagnosticsError).

Modules

A module (NewModule) is a Go module's services, which a product mounts as one — App.With at its defaults, Mount with a Prefix or a Bind. What it declares is qualified with its name: its services are "<module>.<service>", its settings and secrets "<module>.<name>", its routes served under its prefix; a module it Requires is mounted with it, and the modules start first.

Processes

An app has a process profile (Profile): a server (the default) serves HTTP; a daemon serves no HTTP, answers on its listeners and may stop when idle (IdleStop) — no connection open and no Service.Activity busy —; a CLI run opens what one command reads, runs it and exits. A command may be the app's default (DefaultCommand: every argument that names no other command is its) and never fail its shell (FailSafe). Singleton keeps one process of an app per machine, SingletonPer one per user, executable or configuration (PerUID, PerExecutable, PerConfigDir, PerEnv), and SocketPer names a listener's socket the same way (SocketPathFor computes it for a client). Stop ends the run of the app a context runs in. A binary (NewBinary) holds process roles — an app each, selected by the leading arguments — which talk only through a declared contract (Binary.Talks).

Running

App.Main understands its commands — serve, graph, healthcheck, config, secrets, migrate — and the environment: KIT_ENV (dev serves the Studio; production is the default), KIT_ADDR or PORT, KIT_DATA_DIR, KIT_ALLOWED_HOSTS, KIT_LOG_LEVEL. The Studio's pages are the kit tool's, not the product's: in dev a server serves the read-only API the Studio reads, and no route that acts on the product exists. The mechanics underneath — lifecycle, health, scheduler, queue, tracing, logging, validation, resilience, identifiers — are the kitsunium SDK's.

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

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

Package kit — exposures: an operation an endpoint serves over HTTP, and who may run it.

Package kit — reading a record's former values.

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

Package kit — secondary indexes of a store.

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

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

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

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

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

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

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

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

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

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

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

Package kit — export: a person's data as kit gives it back.

Package kit — CPU profiles of the running process, for the Studio.

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

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

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

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

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

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

Package kit — revisions: a record keeps its versions, diffed and restored.

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

Package kit — 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 kit — secrets: the values the product declares and the operator provides.

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

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

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

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

Package kit — frontends: static assets the product serves.

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

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

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

Package kit — transactions: what a unit of work writes commits together, and what it sends leaves at its commit.

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

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

Index

Constants

View Source
const (
	// EnvDev serves the Studio, analyzes the source and binds loopback. Only
	// `kit dev` or an explicit KIT_ENV=dev turns it on.
	EnvDev string = ikit.EnvDev

	// EnvProduction is the default: no Studio, no source, no analysis.
	EnvProduction string = ikit.EnvProduction
)
View Source
const (
	// WireInvalid is WireInvalid.
	WireInvalid string = ikit.WireInvalid

	// WireNotFound is WireNotFound.
	WireNotFound string = ikit.WireNotFound

	// WireConflict is WireConflict.
	WireConflict string = ikit.WireConflict

	// WireUnauth is WireUnauth.
	WireUnauth string = ikit.WireUnauth

	// WireForbidden is WireForbidden.
	WireForbidden string = ikit.WireForbidden

	// WireTooLarge is WireTooLarge.
	WireTooLarge string = ikit.WireTooLarge

	// WireUnsupported is WireUnsupported.
	WireUnsupported string = ikit.WireUnsupported

	// WireRateLimited is WireRateLimited.
	WireRateLimited string = ikit.WireRateLimited

	// WireUnavailable is WireUnavailable.
	WireUnavailable string = ikit.WireUnavailable

	// WireTimeout is WireTimeout.
	WireTimeout string = ikit.WireTimeout

	// WireCanceled is WireCanceled.
	WireCanceled string = ikit.WireCanceled

	// WireInternal is WireInternal.
	WireInternal string = ikit.WireInternal

	// CodeStoreDecode is CodeStoreDecode.
	CodeStoreDecode errs.Code = ikit.CodeStoreDecode

	// CodeStoreEncode is CodeStoreEncode.
	CodeStoreEncode errs.Code = ikit.CodeStoreEncode

	// CodeStorePersist is CodeStorePersist.
	CodeStorePersist errs.Code = ikit.CodeStorePersist

	// CodeStoreLoad is CodeStoreLoad.
	CodeStoreLoad errs.Code = ikit.CodeStoreLoad

	// CodeStoreIndex is CodeStoreIndex.
	CodeStoreIndex errs.Code = ikit.CodeStoreIndex

	// CodeTopicEncode is CodeTopicEncode.
	CodeTopicEncode errs.Code = ikit.CodeTopicEncode

	// CodeTopicPublish is CodeTopicPublish.
	CodeTopicPublish errs.Code = ikit.CodeTopicPublish

	// CodeQueueOpen is CodeQueueOpen.
	CodeQueueOpen errs.Code = ikit.CodeQueueOpen

	// CodeUndecodable is CodeUndecodable.
	CodeUndecodable errs.Code = ikit.CodeUndecodable

	// CodeHandlerPanic is CodeHandlerPanic.
	CodeHandlerPanic errs.Code = ikit.CodeHandlerPanic

	// CodeNotMounted is CodeNotMounted.
	CodeNotMounted errs.Code = ikit.CodeNotMounted

	// CodeWorkflowLoad is CodeWorkflowLoad.
	CodeWorkflowLoad errs.Code = ikit.CodeWorkflowLoad

	// A workflow hook panicked: an OnEnter hook fails its transition with it,
	// an OnTransition hook is reported and the transition stands.
	CodeWorkflowHookPanic errs.Code = ikit.CodeWorkflowHookPanic

	// An OnEnter hook changed the state its entity was entering, or the
	// entity's key: the transition is refused.
	CodeWorkflowHookChange errs.Code = ikit.CodeWorkflowHookChange

	// An OnEnter hook fired its own workflow: refused, where it would wait
	// for itself.
	CodeWorkflowReentrant errs.Code = ikit.CodeWorkflowReentrant

	// CodeAppConfig is CodeAppConfig.
	CodeAppConfig errs.Code = ikit.CodeAppConfig

	// CodeAppRunning is CodeAppRunning.
	CodeAppRunning errs.Code = ikit.CodeAppRunning

	// CodeAppListen is CodeAppListen.
	CodeAppListen errs.Code = ikit.CodeAppListen

	// Mailers.
	CodeMailConfig errs.Code = ikit.CodeMailConfig

	// CodeMailEncode is CodeMailEncode.
	CodeMailEncode errs.Code = ikit.CodeMailEncode

	// CodeMailQueue is CodeMailQueue.
	CodeMailQueue errs.Code = ikit.CodeMailQueue

	// Deprecated: kit 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 = ikit.CodeMailUndecodable

	// CodeMailPanic is CodeMailPanic.
	CodeMailPanic errs.Code = ikit.CodeMailPanic

	// Loops.
	CodeLoopPanic errs.Code = ikit.CodeLoopPanic

	// CodeLoopNotMounted is CodeLoopNotMounted.
	CodeLoopNotMounted errs.Code = ikit.CodeLoopNotMounted

	// CodeLoopStart is CodeLoopStart.
	CodeLoopStart errs.Code = ikit.CodeLoopStart

	// Secrets.
	CodeSecretStore errs.Code = ikit.CodeSecretStore

	// CodeSecretMissing is CodeSecretMissing.
	CodeSecretMissing errs.Code = ikit.CodeSecretMissing

	// CodeSecretRead is CodeSecretRead.
	CodeSecretRead errs.Code = ikit.CodeSecretRead

	// CodeSecretRotate is CodeSecretRotate.
	CodeSecretRotate errs.Code = ikit.CodeSecretRotate

	// Databases, ADR 0004.
	CodeDatabaseOpen errs.Code = ikit.CodeDatabaseOpen

	// CodeDatabaseMigrate is CodeDatabaseMigrate.
	CodeDatabaseMigrate errs.Code = ikit.CodeDatabaseMigrate

	// CodeDatabaseUnavailable is CodeDatabaseUnavailable.
	CodeDatabaseUnavailable errs.Code = ikit.CodeDatabaseUnavailable

	// CodeDatabaseConfig is CodeDatabaseConfig.
	CodeDatabaseConfig errs.Code = ikit.CodeDatabaseConfig

	// CodeTransactionSpan is CodeTransactionSpan.
	CodeTransactionSpan errs.Code = ikit.CodeTransactionSpan

	// Privacy (ADR 0006).
	CodePrivacyKey errs.Code = ikit.CodePrivacyKey

	// CodePrivacyErase is CodePrivacyErase.
	CodePrivacyErase errs.Code = ikit.CodePrivacyErase

	// CodePrivacyJournal is CodePrivacyJournal.
	CodePrivacyJournal errs.Code = ikit.CodePrivacyJournal

	// CodePrivacyData is CodePrivacyData.
	CodePrivacyData errs.Code = ikit.CodePrivacyData

	// History (ADR 0007).
	CodeHistoryLoad errs.Code = ikit.CodeHistoryLoad

	// CodeHistoryWrite is CodeHistoryWrite.
	CodeHistoryWrite errs.Code = ikit.CodeHistoryWrite

	// CodeHistoryRead is CodeHistoryRead.
	CodeHistoryRead errs.Code = ikit.CodeHistoryRead

	// CodePasswordHash is CodePasswordHash.
	CodePasswordHash errs.Code = ikit.CodePasswordHash

	// Commands and queries.
	CodeCommandReentrant errs.Code = ikit.CodeCommandReentrant

	// CodeCommandKey is CodeCommandKey.
	CodeCommandKey errs.Code = ikit.CodeCommandKey

	// CodeCommandQueue is CodeCommandQueue.
	CodeCommandQueue errs.Code = ikit.CodeCommandQueue

	// CodeCommandEncode is CodeCommandEncode.
	CodeCommandEncode errs.Code = ikit.CodeCommandEncode

	// Watches (ADR 0008).
	CodeWatchQueue errs.Code = ikit.CodeWatchQueue

	// Sealing at rest (ADR 0006, step 3).
	CodeSealKey errs.Code = ikit.CodeSealKey

	// CodeSealWrite is CodeSealWrite.
	CodeSealWrite errs.Code = ikit.CodeSealWrite

	// CodeSealOpen is CodeSealOpen.
	CodeSealOpen errs.Code = ikit.CodeSealOpen

	// CodeSealRewrap is CodeSealRewrap.
	CodeSealRewrap errs.Code = ikit.CodeSealRewrap

	// CodeSealShred is CodeSealShred.
	CodeSealShred errs.Code = ikit.CodeSealShred

	// Revisions (ADR 0007 §3).
	CodeRevisionRead errs.Code = ikit.CodeRevisionRead

	// CodeRevisionWrite is CodeRevisionWrite.
	CodeRevisionWrite errs.Code = ikit.CodeRevisionWrite

	// CodeRevisionDecode is CodeRevisionDecode.
	CodeRevisionDecode errs.Code = ikit.CodeRevisionDecode

	// 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 = ikit.CodeJobOverlapped

	// CodeHistoryMoved is CodeHistoryMoved.
	CodeHistoryMoved errs.Code = ikit.CodeHistoryMoved

	// CodeNotSource is CodeNotSource.
	CodeNotSource errs.Code = ikit.CodeNotSource

	// CodePasswordMoved is CodePasswordMoved.
	CodePasswordMoved errs.Code = ikit.CodePasswordMoved

	// CodeNothingToErase is CodeNothingToErase.
	CodeNothingToErase errs.Code = ikit.CodeNothingToErase

	// CodeKeyRekeyed is CodeKeyRekeyed.
	CodeKeyRekeyed errs.Code = ikit.CodeKeyRekeyed

	// CodeMigrateRefused is CodeMigrateRefused.
	CodeMigrateRefused errs.Code = ikit.CodeMigrateRefused

	// CodeCatalogue is CodeCatalogue.
	CodeCatalogue errs.Code = ikit.CodeCatalogue

	// CodeRetentionPanic is CodeRetentionPanic.
	CodeRetentionPanic errs.Code = ikit.CodeRetentionPanic

	// The signals kit ends one of its own steps with and catches itself — a
	// caller never receives one —, typed when the SDK made rule 2 a gate.
	CodeSealErased errs.Code = ikit.CodeSealErased

	// CodeSealKeyMoved is CodeSealKeyMoved.
	CodeSealKeyMoved errs.Code = ikit.CodeSealKeyMoved

	// CodeResealInPlace is CodeResealInPlace.
	CodeResealInPlace errs.Code = ikit.CodeResealInPlace

	// CodeResealMoved is CodeResealMoved.
	CodeResealMoved errs.Code = ikit.CodeResealMoved

	// CodeWorkflowNotInPlace is CodeWorkflowNotInPlace.
	CodeWorkflowNotInPlace errs.Code = ikit.CodeWorkflowNotInPlace

	// CodeTransactionPanic is CodeTransactionPanic.
	CodeTransactionPanic errs.Code = ikit.CodeTransactionPanic
)
View Source
const CodeCLIFailed errs.Code = ikit.CodeCLIFailed

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

View Source
const CodeProfileRead errs.Code = ikit.CodeProfileRead

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 CodeScopeUnknown errs.Code = ikit.CodeScopeUnknown

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 = ikit.CodeSingletonHeld

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

View Source
const DefaultMaxBody int64 = ikit.DefaultMaxBody

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

Variables

This section is empty.

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 *ikit.PortService[Req, Resp], op ikit.Operation[Req, Resp]) interface {
	AppOption
	MountOption
}

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.

app := kit.NewApp("vigie", desk.Service, posts.Service).With(kit.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.

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

func Catalog

func Catalog() []model.Mechanic

Catalog lists the generic mechanics kit 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 KIT_TRUST_PROXY=on — the last address of X-Forwarded-For, the one the trusted proxy appended. It is "" outside an HTTP request.

func Fallback

func Fallback[Req, Resp any](op ikit.Operation[Req, Resp]) ikit.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", kit.Fallback(QueueAPI))

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

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 {
	StoreOption
	AppOption
}

InMemory keeps data in memory. On an app, it overrides the data directory for every store and queue — what a test wants. On a store, it keeps that store in memory even when the app has a data directory.

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 Migrations

func Migrations(m ...sql.Migration) interface {
	DatabaseOption
	ModulePart
}

Migrations are migrations, values of the SDK's sql.Migration, run when the database starts (<name>-migrate: start) or by `<product> migrate up`, kit'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.

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 kit'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 kit 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 ScopeKey

func ScopeKey(scopes ...Scope) (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. kit 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 Endpoint.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 kit 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 ...Scope) (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 added in v0.14.1

func Transact(ctx context.Context, fn func(context.Context) error) 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 := kit.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 after 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. One a queue refuses after the commit 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. On the data directory and in memory kit 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 Activity

type Activity = ikit.ActivityHandler

Activity 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 = ikit.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 kit tool gives one, in dev, through Analyzer.

type App

type App = ikit.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.

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.

type AppOption

type AppOption = ikit.AppConfigurer

AppOption configures an app. Options win over the environment.

func Analyze

func Analyze(on bool) AppOption

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

func Analyzer

func Analyzer(fn AnalyzeFunc) AppOption

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) AppOption

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) AppOption

ConfigFiles gives kit the product's configuration files: config/config.<ext>, which every environment reads, and config/<env>.<ext>, the environment's own — <env> being KIT_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

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

func DataDir

func DataDir(dir string) AppOption

DataDir sets where stores and queues persist, instead of KIT_DATA_DIR.

func Database

func Database(name string, engine Engine, opts ...DatabaseOption) AppOption

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 = kit.NewApp("vigie", intake.Service, desk.Service, audit.Service).With(
	// Keeps every store no other database keeps.
	kit.Database("database", postgres.Engine()),
	// The audit trail apart.
	kit.Database("archive", sqlite.Engine(), kit.Keeps(audit.Service)),
)

name is 1 to 49 lower-case letters, digits and dashes, not starting with "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) 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) AppOption

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) AppOption

Env selects the environment, instead of KIT_ENV.

func IdleStop

func IdleStop(d time.Duration) AppOption

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) AppOption

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

func Logs

func Logs(w io.Writer) AppOption

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

func Mount

func Mount(m *Module, opts ...MountOption) AppOption

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

app := kit.NewApp("vigie", identity.Service).
	With(kit.Mount(moderation.Module, kit.Prefix("/"), kit.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 Profile

func Profile(p string) AppOption

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

func Replace

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

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(kit.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) AppOption

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[V any](name string, value V) AppOption

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) AppOption

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 ...Scope) AppOption

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) AppOption

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

func Telemetry

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

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 KIT_TELEMETRY.

type Authenticator

type Authenticator[P, D any] = ikit.AuthenticatorHandler[P, D]

Authenticator 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 Binary

type Binary = ikit.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 = kit.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.

type CLI

type CLI = ikit.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 CLIFunc

type CLIFunc = ikit.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 CLIOption

type CLIOption = ikit.CommandLineConfigurer

CLIOption tunes a CLI command.

func DefaultCommand

func DefaultCommand() CLIOption

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() CLIOption

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 Change

type Change[E any, S comparable] = ikit.ChangeEvent[E, S]

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

type Command

type Command[C, R any] = ikit.Command[C, R]

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 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 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.

ADR 0004's step 2 makes a command its unit of work: its authorization and its handler will run in one transaction, which holds its effects until the commit. Until then, a command's writes and effects are made as the handler makes them.

type CommandOption

type CommandOption = ikit.CommandConfigurer

CommandOption 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 added in v0.14.1

func NoTransaction() CommandOption

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() CommandOption

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 after half a second up to MaxDeliveries attempts (5), then dead-lettered. It answers nothing: its result is Empty, 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 DatabaseOption

type DatabaseOption = ikit.DatabaseConfigurer

DatabaseOption configures a database: Keeps, Migrations.

func Keeps

func Keeps(things ...Keepable) DatabaseOption

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.

type DatabaseURL

type DatabaseURL = ikit.DatabaseURLValue

DatabaseURL 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 = ikit.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 DeliveryOption

type DeliveryOption = ikit.DeliveryOption

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

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.

type DiagnosticsError

type DiagnosticsError = ikit.DiagnosticsError

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

type Edit added in v0.14.1

type Edit = ikit.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 Empty

type Empty = ikit.EmptyValue

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

type Endpoint

type Endpoint[Req, Resp any] = ikit.EndpointService[Req, Resp]

Endpoint is an HTTP endpoint with a typed request and a typed response. Declare it with Service.Endpoint; call it from another service with Endpoint.Call, which is how the diagram sees service-to-service calls.

type EndpointOption

type EndpointOption = ikit.EndpointConfigurer

EndpointOption configures an endpoint.

func MaxBody

func MaxBody(bytes int64) EndpointOption

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

func Name

func Name(name string) EndpointOption

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 Engine

type Engine = ikit.Engine

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

type Erasure

type Erasure = ikit.Erasure

Erasure is what kit.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 kit.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.

Once sealing lands (ADR 0006, step 3), an erasure also moves the held records under keys of their own and destroys the subject's data key, so that it reaches every copy kit sealed.

type Error

type Error = ikit.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.

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 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).

type ExposeOption added in v0.14.1

type ExposeOption = ikit.ExposeConfigurer

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

func AnyUser added in v0.14.1

func AnyUser() ExposeOption

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, kit.Auth()).Expose("GET /orders", kit.AnyUser())

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

func Anyone added in v0.14.1

func Anyone() ExposeOption

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", kit.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 FieldRef

type FieldRef = ikit.FieldRefValue

FieldRef 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 = ikit.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 = ikit.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 Job

type Job = ikit.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 Keepable

type Keepable = ikit.Keeper

Keepable 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 = ikit.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 Listener

type Listener = ikit.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 ListenerOption

type ListenerOption = ikit.ListenerConfigurer

ListenerOption tunes a listener.

func AllowPeers

func AllowPeers(uids, gids []int) ListenerOption

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) ListenerOption

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 ...Scope) ListenerOption

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 Loop

type Loop = ikit.Loop

Loop is a declared loop of the daemon: kit 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; kit runs it on its own goroutine, one run at a time, observes every run, and restarts it after a panic.

type LoopOption

type LoopOption = ikit.LoopConfigurer

LoopOption configures a declared loop.

func WakeAt

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

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) LoopOption

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 *ikit.TopicService[T]) LoopOption

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 = ikit.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 — KIT_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 KIT_SMTP_URL_FILE — takes effect at the next mail.

type MailerOption

type MailerOption = ikit.MailerConfigurer

MailerOption configures a mailer.

func From

func From(name, addr string) MailerOption

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) MailerOption

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.

type Mark

type Mark = ikit.Mark

Mark is what a module looks for in the product's fields: kit.Personal, kit.Special or kit.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 = ikit.Personal

	// Special marks a field of GDPR art. 9(1) or art. 10.
	Special Mark = ikit.Special

	// Moderated marks content others see and a moderator may act on.
	Moderated Mark = ikit.Moderated
)

type Module

type Module = ikit.Module

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 ...ModulePart) *Module

NewModule declares a module named name — lower-case letters, digits and dashes, starting with a letter; "kit", "external" and "schema" are kit'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 kit'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 = kit.NewModule("moderation",
	"Reports and moderation under the DSA.\n\nfr: Signalements et modération selon le DSA.",
	intake.Service, desk.Service, kit.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.

type ModulePart

type ModulePart = ikit.ModuleConfigurer

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

func Requires

func Requires(other *Module) ModulePart

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 MountOption

type MountOption = ikit.MountConfigurer

MountOption configures the mount of a module: Prefix, Bind.

func Prefix

func Prefix(prefix string) MountOption

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 /_kit/.

type Operation

type Operation[Req, Resp any] = ikit.Operation[Req, Resp]

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 kit 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 = ikit.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).

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) OperationOption

Bulkhead caps how many requests the handler serves at once; the overflow answers 503. It is the SDK's resilience bulkhead.

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) 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. It is the SDK's resilience rate limiter.

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 Endpoint.Call share one bucket. One client emptying its bucket is answered 429; 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.

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) OperationOption

Timeout cancels the handler's context after d and answers 504. It is the SDK's resilience timeout.

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

type PasswordOption

type PasswordOption = ikit.PasswordConfigurer

PasswordOption configures a password policy: MinLength, NotReused.

func MinLength

func MinLength(n int) PasswordOption

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 added in v0.14.1

func NotCommon() PasswordOption

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.

func NotReused

func NotReused(n int) PasswordOption

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. kit 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 PasswordPolicy

type PasswordPolicy[T any] = ikit.PasswordPolicyService[T]

PasswordPolicy 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. Store.Passwords declares it; Set, Change and Verify apply it.

type PersonalData

type PersonalData = ikit.PersonalData

PersonalData is a person's data as kit.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 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 Port

type Port[Req, Resp any] = ikit.PortService[Req, Resp]

Port 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 Port.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 PortOption

type PortOption[Req, Resp any] = ikit.PortConfigurer[Req, Resp]

PortOption configures a port: Fallback.

type Principal

type Principal = ikit.AttrsProvider

Principal 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...)} }

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

type Query

type Query[Q, R any] = ikit.Query[Q, R]

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. 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 QueryOption

type QueryOption = ikit.QueryConfigurer

QueryOption configures a query: the options every operation takes — Auth, AuthOptional, RateLimit, RateLimitPerClient, Timeout, Bulkhead.

type Records

type Records = ikit.RecordsService

Records is one store's records without their type. Get returns a record without its secret members; 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 RecordsOf

func RecordsOf(ctx context.Context, store string) (Records, 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 kit.Written) error {
	recs, err := kit.RecordsOf(ctx, w.Store)
	…
	record, err := recs.Get(ctx, w.Key)

type Revision added in v0.14.1

type Revision[T any] = ikit.RevisionEvent[T]

Revision 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 = ikit.Routine

Routine is a loop written by hand: a function that runs until its context ends. kit 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 Scope

type Scope = ikit.ScopeValue

Scope 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.

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

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

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

func PerEnv

func PerEnv(name, fallback string) Scope

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 = ikit.Secret

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

type SecretOption

type SecretOption = ikit.SecretConfigurer

SecretOption configures a secret.

func Generated

func Generated(bytes int) SecretOption

Generated makes the secret kit'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) SecretOption

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 added in v0.14.1

func Optional() SecretOption

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 kit's to make and never absent: Optional with Generated is refused.

func RotateEvery

func RotateEvery(d time.Duration) SecretOption

RotateEvery is how often kit 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 = ikit.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 = kit.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.

var Privacy *Service = ikit.Privacy

Privacy is kit's own service: the legal holds and the privacy journal of an app that keeps personal data. kit mounts a copy of it — named kit.privacy, a name no service of a product can take — in every app whose stores keep personal data or declare a retention, before the product's services. Its stores stay in the data directory unless the product places them (ADR 0004). A product never mounts it itself.

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.

type Setting

type Setting[T SettingValue] = ikit.SettingService[T]

Setting is a value the environment gives a service, declared with Service.Setting.

type SettingOption

type SettingOption = ikit.SettingConfigurer

SettingOption configures a setting.

func Required

func Required() SettingOption

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 SettingValue

type SettingValue = ikit.SettingValue

SettingValue is what a setting may hold.

type StaticOption

type StaticOption = ikit.StaticConfigurer

StaticOption configures a frontend.

func Root

func Root(dir string) StaticOption

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 Stdio

type Stdio = ikit.StdioValue

Stdio is where a CLI command reads and writes: the process's own streams under Main, buffers in a test.

type Store

type Store[T any] = ikit.StoreService[T]

Store 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 (KIT_DATA_DIR; ".kit/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 is a port (ADR 0004): what it runs on is its engine (store_engine.go), today the SDK's document store (docstore); kit makes it a node of the graph, observes every call, and speaks for its refusals.

type StoreOption

type StoreOption = ikit.StoreConfigurer

StoreOption configures a store.

func Anonymise

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

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 kit clears the rest. kit 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)) StoreOption

DeleteAfter deletes a record delay after the instant since returns.

func DeleteAt

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

DeleteAt deletes a record at the instant at returns.

func DeleteOnErasure

func DeleteOnErasure() StoreOption

DeleteOnErasure makes a person's erasure (kit.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)) StoreOption

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)) StoreOption

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) StoreOption

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) StoreOption

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

func Purpose

func Purpose(text string) StoreOption

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() StoreOption

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. kit'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, kit.ReadModel())

func RetentionByProduct added in v0.14.1

func RetentionByProduct(limits string) StoreOption

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 kit 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.

kit 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 kit.Erase and Store.Erase. It goes with kit.Purpose, kit.HeldUntil, kit.Anonymise and kit.DeleteOnErasure; with kit.EraseAfter, kit.EraseAt, kit.DeleteAfter or kit.DeleteAt it is refused: a store's retention is kit's or the product's, never both.

func Revisions added in v0.14.1

func Revisions(n int) StoreOption

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, kit.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 kit's migrations create.

func Unique

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

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. Store.Lookup reads it.

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

type Subscription

type Subscription[T any] = ikit.SubscriptionWorker[T]

Subscription consumes a topic: its handler runs once per message, at least once, retried on error, and dead-lettered after MaxDeliveries attempts.

type SubscriptionOption

type SubscriptionOption = ikit.SubscriptionConfigurer

SubscriptionOption configures a subscription.

func OwnStores added in v0.14.1

func OwnStores() SubscriptionOption

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 Topic

type Topic[T any] = ikit.TopicService[T]

Topic 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 UID

type UID = ikit.UID

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 Endpoint.Call carries its caller's user along.

type Violation

type Violation = ikit.ViolationMessage

Violation is one validation rule a request failed: where, which rule, and why — never the value.

type Wake

type Wake = ikit.WakeEvent

Wake says why a declared loop runs: the reason, the instant, and the topic that woke it when one did.

type Watch

type Watch = ikit.Watch

Watch is a subscription fed by stores instead of a topic: its handler is told of every write kit confirms on a store whose entity holds a field with its mark. Service.Watch declares one.

type Workflow

type Workflow[E any, S comparable] = ikit.WorkflowService[E, S]

Workflow 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:

  • event transitions (Workflow.On), fired by code with Workflow.Fire;
  • timer transitions (Workflow.After), fired by the workflow's own loop once an entity has spent a duration in a state;
  • timer transitions at an instant the entity carries (Workflow.At) — a due date, a deadline — fired by the same loop when it comes;
  • guard transitions (Workflow.When), fired by the same loop as soon as a condition on the entity holds.

The SDK's state-machine engine runs it (statemachine). 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 Written

type Written = ikit.WrittenEvent

Written 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.

Directories

Path Synopsis
config
toml
Package toml lets a kit product read TOML configuration files — config/config.toml, config/<env>.toml — as it reads JSON ones.
Package toml lets a kit product read TOML configuration files — config/config.toml, config/<env>.toml — as it reads JSON ones.
yaml
Package yaml lets a kit product read YAML configuration files — config/config.yaml, config/<env>.yaml, and .yml — as it reads JSON ones.
Package yaml lets a kit product read YAML configuration files — config/config.yaml, config/<env>.yaml, and .yml — as it reads JSON ones.
Package server makes a kit app in the server profile — the default — serve HTTP: its endpoints, its frontends and its health probes, on the SDK's server engine.
Package server makes a kit app in the server profile — the default — serve HTTP: its endpoints, its frontends and its health probes, on the SDK's server engine.
Package storetest is the conformance suite of kit's stores (ADR 0004): what a store does on every backend — in memory, in the data directory, on each database engine —, run by kit's tests on memory, files and kit's fake database, and by each engine module's tests on its engine.
Package storetest is the conformance suite of kit's stores (ADR 0004): what a store does on every backend — in memory, in the data directory, on each database engine —, run by kit's tests on memory, files and kit's fake database, and by each engine module's tests on its engine.
Package studio serves, in dev, the read-only API the kit tool's Studio reads: the graph and its event stream, traces, instances and items, the source, the dev tools — the process, the databases, goroutines, the heap profile, logs, the mailbox, former values.
Package studio serves, in dev, the read-only API the kit tool's Studio reads: the graph and its event stream, traces, instances and items, the source, the dev tools — the process, the databases, goroutines, the heap profile, logs, the mailbox, former values.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL