Skip to content

How are storage, signalling and identity cut into ports?

A port is a small Go interface, a set of semantics every adapter shares and a conformance suite. The contracts are in the ports reference.

You want Read
the keys sluis stores keys
where each record lands in DynamoDB, SSM and S3 storage layout
one adapter's settings, IAM and limits adapter details
which adapters and presets exist adapters
how far each piece has got, per platform capabilities
how the domain records sit on the ports the domain stores

What is a port?

Business code names a port and never an adapter, and an adapter holds no business rule. The test internal/port/guard_test.go fails on a business import of internal/kube, internal/valkey or the legacy adapter. Only the adapters, the factory internal/store, the apps that assemble a process, the migration and the commands may.

Every port has an adapter for each permanent platform:

Port What it is for Kubernetes AWS Lambda
State records, sessions, tokens, leases, gates, caches, counters (no credential) legacy (ConfigMaps, Valkey), DynamoDB DynamoDB
Blob status reports, directory snapshots S3 S3
Trigger a change becomes a tick DynamoDB (polling watch), in-process asynchronous lambda:Invoke
Secrets dynamic secrets OpenBao, SSM, memory, legacy SSM, OpenBao
Inputs policy, configuration, operator-managed secrets mounted ConfigMaps and Secrets the configuration layer, or a parameter store
Identity proves a workload to the issuer, and the service to the cloud ServiceAccount token, AWS federation the same
Audit sink records what the service did connect, log sqs

Adapters is generated from the registry (port.Default.Matrix()) and wins on a difference.

What do the ports promise?

State is single-key with a lifetime. A flow over several keys is a sequence of idempotent steps with a recovery marker. Every record expires unless the layout says it is permanent. A revision is opaque and changes on every write.

Reads filter on expiry because DynamoDB removes items late. A credential is never written to State. The domain stores put it in Secrets and leave a marker in the record.

Who owns a record, and who may read it?

Each module owns one table (ADR 0072): oidc, github, slack, cloudflare, google and backup. A process writes its own module's table, and another module's key fails with ErrNotOwner before any request.

A peer is a read-only view of another module's table. The issuer holds google, github and slack. Any other module's read fails.

A listing across modules, such as ws., is ErrUnsupported on the router: list ws.dir.. The dynamodb adapter needs ports.dynamodb.tables, and the ssm source needs secrets.layout: v5; start refuses either without it.

DynamoDB has no cheap change feed, so Watch and the Trigger poll the prefix. A notification is a hint, and the lease and the backstop schedule make a duplicate or lost one harmless.

The legacy adapter wraps ConfigMaps, Secrets and Valkey, and goes when the migration has run. One conformance suite gates every adapter and the migration tool (run it; assertions).

Adapters, presets and the platform

You choose an adapter per concern: state, secrets, blobs, signing, trigger, schedule and audit. Each adapter registers a descriptor in internal/port (port.Register) with its name, concern, needs, runtimes (kubernetes, lambda, process), status (implemented, on-request) and factory.

The aws-hybrid and aws-serverless presets run sluis on Lambda with DynamoDB state, SSM secrets, KMS-wrapped signing and S3 blobs. They add SQS audit, EventBridge ticks and an asynchronous invoke for "run a pass now". Every other adapter is on request: in the matrix and refused at start.

How do you choose a preset?

Answer four questions in order:

AWS?        no  -> Kubernetes?   no  -> server
                                 yes -> OpenBao?  yes -> k8s-openbao
                                                  no  -> k8s-minimal
            yes -> Kubernetes?   no  -> aws-serverless
                                 yes -> sluis on Lambda?  yes -> aws-hybrid
                                                          no  -> k8s-aws

The answers are the platform block (aws, kubernetes, openbao, runtime, replicas), or preset names one. A modifier overrides one concern: adapters.secrets: {adapter: openbao, settings: {...}} replaces the SSM secrets of k8s-aws with OpenBao, with no platform.openbao.

Available presets: aws-serverless, aws-hybrid and k8s-aws (aws-eks is its deprecated name). Loading server, k8s-minimal or k8s-openbao fails and names the missing adapters. The adapters page lists each preset's adapters.

What wins when settings disagree?

From first to last: adapters.<concern> with its settings, the legacy keys beside a preset (ports.adapter, ports.blob), preset, then the preset platform leads to.

With none of platform, preset and adapters, the ports keys decide: ports.adapter: legacy, signing file, schedule ticker, and audit connect when audit.writer is set, else log.

What does start refuse?

When platform or preset is given, start fails and names every problem. It refuses an adapter that needs a false platform answer or cannot run on the runtime (legacy on lambda). It also refuses memory above one replica, a secrets adapter that is not a secret store while the platform has one, and an unknown or planned adapter.

The runtime is platform.runtime, the preset's, or lambda when AWS_LAMBDA_FUNCTION_NAME is set. Start logs the resolved table and exposes the gauge sluis_adapter_info{concern,adapter} 1.

Which signing keys can you publish without signing?

The signing concern chooses file, kms or kms-wrapped. The list signingKey.verifyOnly holds public keys that are published and never signed with, so tokens from old file keys verify across a cutover to kmsWrapped. A private key there stops the start. The keys are in configuration.

Decided in