Skip to content

Storage ports ​

Wicket abstracts all state that needs to be persisted into Go interfaces (ports), and the host injects implementations through ServerConfig. This page is a quick reference for these interfaces: what each one manages, which methods it has, what it returns on errors, and the semantics adapters must follow. To actually write an adapter, continue with Writing a storage adapter.

The ports fall into three families, each with its own sentinel errors:

FamilyPackageInterfaces
Authorization datagithub.com/gonewx/wicket/storage7 authorization state stores + message store + configuration stores
Sessiongithub.com/gonewx/wicket/sessionsession.Store
Key recordsgithub.com/gonewx/wicket/keymgmtkeymgmt.RecordStore

There's also services.DistributedCache, which doesn't belong to any of the three families but must be injected as well; it's covered separately at the end of this page.

Sentinel errors ​

PackageErrorMeaning
storageErrNotFoundA single-record read found nothing
storageErrDuplicateHandleAn insert-only write hit an existing handle
storageErrVersionConflictVersion mismatch during an optimistic concurrency update
sessionErrSessionNotFoundThe session doesn't exist
keymgmtErrRecordNotFoundThe key record doesn't exist
keymgmtErrDuplicateKeyDuplicate handle or duplicate public kid of a non-retired record
keymgmtErrVersionConflictKey record version conflict

Note that "not found" is three different values across the three families: a session store returning storage.ErrNotFound won't be recognized. The keymgmt package also defines ErrNoSigningKey, ErrAlgorithmUnavailable, ErrConfiguration, and ErrProtectorUnavailable; these are returned by the key manager and aren't for storage adapters to use. Always check with errors.Is; adapters may wrap them (fmt.Errorf("...: %w", storage.ErrNotFound)).

Contract semantics ​

The following rules hold for all three families of ports (DistributedCache is an exception; see the end).

"Not found" and "failure" must be distinguishable. When a single-record read finds nothing, return the corresponding sentinel error, never (nil, nil); return other errors for things like a failed database connection. Otherwise a network blip is treated by the core as "invalid authorization code": the user sees a login failure, and operations get no alert at all. List methods with no results return an empty slice rather than a sentinel (the storage family requires a non-nil empty slice; the session family accepts either nil or an empty slice).

Insert-only writes. Writes keyed by a caller-generated handle only insert and never overwrite; if the handle already exists, return a conflict error and leave the original record unchanged. The single-use semantics of refresh tokens and authorization codes depend on this; implementing it as an upsert (for example a plain SET in a key-value store) silently disables replay protection. Only two methods are explicitly declared as upserts: PersistedGrantStore.Store (by grant key) and UserConsentStore.StoreUserConsent (by subject + client).

Version-based concurrency control. Read-modify-write updates carry an expectedVersion: write only if the stored version equals it, increment the version, and write it back to the caller's object; otherwise return ErrVersionConflict and leave the record unchanged. The storage layer doesn't resolve conflicts itself. For refresh tokens, for example, the Wicket core doesn't retry; it turns the conflict into invalid_grant at the token endpoint—of two concurrent refresh requests, only one succeeds.

Timeouts are determined by the caller's ctx. The first parameter of every method is a context.Context, and cancellation and deadlines must be passed all the way down to the underlying driver. Adapters must not create their own context.Background(), nor set timeouts that override the caller's.

Expiry is decided on the core side. Reads return records as usual whether or not they've expired; don't add conditions such as expires_at > now() to queries. Database clocks and application node clocks drift, so expiry is consistent only when the core decides it using the injected clock. Data with a lifetime provides RemoveExpired / DeleteExpired(ctx, cutoff) for reclaiming space; it deletes only records older than cutoff, returns the number deleted, and isn't responsible for correctness.

No self-started background tasks. Adapters must not start background goroutines or timers; the host calls the reclamation methods above on its own schedule. The conformance suite checks this.

Concurrency safety. Store instances are singletons in the container and are called concurrently by all requests.

storage family: authorization state ​

The model types live in github.com/gonewx/wicket/storage/models.

AuthorizationCodeStore ​

go
StoreAuthorizationCode(ctx, handle string, code *models.AuthorizationCode) error // insert-only
GetAuthorizationCode(ctx, handle string) (*models.AuthorizationCode, error)      // returns an independent copy
RemoveAuthorizationCode(ctx, handle string) error
RemoveExpired(ctx, cutoff time.Time) (int, error) // creation time + lifetime earlier than cutoff

RefreshTokenStore ​

go
StoreRefreshToken(ctx, handle string, token *models.RefreshToken) error // insert-only; stored copy gets version 1, written back
GetRefreshToken(ctx, handle string) (*models.RefreshToken, error)
UpdateRefreshToken(ctx, handle string, token *models.RefreshToken, expectedVersion int) error // version-controlled
RemoveRefreshToken(ctx, handle string) error
RemoveExpired(ctx, cutoff time.Time) (int, error)

This is the only interface in the storage family with a version number.

ReferenceTokenStore ​

go
StoreReferenceToken(ctx, handle string, token *models.Token) error // insert-only
GetReferenceToken(ctx, handle string) (*models.Token, error)
RemoveReferenceToken(ctx, handle string) error
RemoveReferenceTokens(ctx, subjectId, clientId string) error // bulk revocation by subject + client
RemoveExpired(ctx, cutoff time.Time) (int, error)

UserConsentStore ​

go
StoreUserConsent(ctx, consent *models.Consent) error // upsert: by subject + client
GetUserConsent(ctx, subjectId, clientId string) (*models.Consent, error)
GetAllUserConsents(ctx, subjectId string) ([]*models.Consent, error)
RemoveUserConsent(ctx, subjectId, clientId string) error
RemoveExpired(ctx, cutoff time.Time) (int, error) // only records with Expiration set

PersistedGrantStore ​

go
Store(ctx, grant *models.PersistedGrant) error // upsert: by grant.Key
Get(ctx, key string) (*models.PersistedGrant, error)
GetAll(ctx, filter *storage.PersistedGrantFilter) ([]*models.PersistedGrant, error)
Remove(ctx, key string) error
RemoveAll(ctx, filter *storage.PersistedGrantFilter) error
RemoveExpired(ctx, cutoff time.Time) (int, error)

The fields of PersistedGrantFilter are SubjectId, SessionId, ClientId, ClientIds, Type, Types; GetAll / RemoveAll must match all non-empty conditions exactly.

DeviceFlowStore ​

go
StoreDeviceAuthorization(ctx, deviceCode, userCode string, data *models.DeviceCode) error // insert-only; fails if either code conflicts
FindByUserCode(ctx, userCode string) (*models.DeviceCode, error)
FindByDeviceCode(ctx, deviceCode string) (*models.DeviceCode, error)
UpdateByUserCode(ctx, userCode string, data *models.DeviceCode) error // state transition, no version number
RemoveByDeviceCode(ctx, deviceCode string) error // clears the indexes for both codes
RemoveExpired(ctx, cutoff time.Time) (int, error)

BackchannelAuthenticationRequestStore ​

go
StoreBackchannelAuthenticationRequest(ctx, requestID string, data *models.BackchannelAuthenticationRequest) error // insert-only
FindBackchannelAuthenticationRequest(ctx, requestID string) (*models.BackchannelAuthenticationRequest, error)
UpdateBackchannelAuthenticationRequest(ctx, requestID string, data *models.BackchannelAuthenticationRequest) error // no version number
RemoveBackchannelAuthenticationRequest(ctx, requestID string) error
RemoveExpired(ctx, cutoff time.Time) (int, error)

Updates in the device flow and CIBA carry no version number: their single-use semantics are guaranteed by status fields and deletion.

Deleting a record that doesn't exist: Wicket's built-in in-memory implementations return nil (idempotent).

storage family: messages and configuration ​

InterfaceMethodsDescription
AuthorizationParametersMessageStoreWrite(ctx, *messages.Message[url.Values]) (string, error), Read(ctx, handle), Delete(ctx, handle)Preserves authorization parameters across the login redirect. When not injected, the parameters go directly into the callback URL
MessageStore[T]Write, ReadInternal messages (logout, errors, etc.); in v0.1.4 handled by built-in implementations, with no injection seam
ClientStoreFindClientById(ctx, clientId)Built from the SetClients data; no separate injection seam
ResourceStoreGetAllEnabledResources, FindResourcesByScope, FindApiResourcesByName, FindApiScopesByName, FindIdentityResourcesByScopeBuilt from SetIdentityScopes and related data; no separate injection seam
SigningCredentialStoreGetSigningCredentials(ctx) (*tokens.SigningCredentials, error)See Signing keys
ValidationKeysStoreGetValidationKeys(ctx) ([]*models.SecurityKeyInfo, error)Same as above

Message and configuration data have no server-side lifetime, so there are no reclamation methods.

session family ​

go
type Store interface {
    GetSession(ctx, sessionID string) (*session.Record, error) // returns ErrSessionNotFound when not found
    CreateSession(ctx, s *session.Record) error                // insert-only
    UpdateSession(ctx, s *session.Record) error
    DeleteSession(ctx, sessionID string) error
    GetSessionsBySubjectID(ctx, subjectID string) ([]*session.Record, error)
    DeleteSessionsBySubjectID(ctx, subjectID string) (int, error)
    AddClientID(ctx, sessionID, clientID string) error // atomic append with deduplication
    DeleteExpired(ctx, cutoff time.Time) (int, error)
}

Differences from the storage family:

  • On a duplicate id, CreateSession is only required to return a non-nil error and leave the original record unchanged; no specific sentinel is prescribed.
  • UpdateSession, DeleteSession, and AddClientID are no-ops on a session that doesn't exist; they don't return an error.
  • There's no version number; the atomicity of AddClientID is guaranteed by the store itself (for example a single-statement SQL array append, or a transaction).
  • Expiry is decided in session.Coordinator.ValidateSession using the injected clock; the store only saves Expires.

Fields of session.Record: SessionID, SubjectID, DisplayName, Created, Renewed, Expires, ClientIDs.

keymgmt family ​

go
type RecordStore interface {
    List(ctx) ([]*keymgmt.Record, error)
    Get(ctx, handle string) (*keymgmt.Record, error)       // returns ErrRecordNotFound when not found
    Create(ctx, record *keymgmt.Record) error               // insert-only; Version is 1 on success
    Update(ctx, record *keymgmt.Record, expectedVersion uint64) error
    Delete(ctx, handle string) error                        // returns ErrRecordNotFound when not found
}

Key points:

  • Create returns ErrDuplicateKey when the handle is a duplicate, or when the PublicID (the kid in the JWKS) duplicates that of a non-retired record; the kid of a retired record may be reused.
  • Update and Delete on a record that doesn't exist return ErrRecordNotFound—the opposite of the session family's "no-op".
  • Version is a uint64 incremented on every write, and is unrelated to the Format field, which describes the private key protection scheme.
  • All return values must be deep copies; caller modifications must not affect stored contents.
  • The store doesn't filter by lifetime window and has no reclamation method: key retirement is driven by the manager on the core side, and the store keeps records until they're explicitly Deleted.
  • ProtectedPrivateMaterial is the private key bytes, already encrypted and protected; store and return them as-is. PublicMaterial is the plaintext public key.

DistributedCache (the exception) ​

go
type DistributedCache interface {
    Get(key string) ([]byte, error)
    Set(key string, value []byte, opts *services.DistributedCacheEntryOptions) error
    Remove(key string) error
}

In v0.1.4 it's used only for device flow polling throttling, and its contract differs from the three families above:

  • The methods take no context.Context parameter;
  • A miss returns (nil, nil), not a sentinel error;
  • The cache decides expiry itself based on AbsoluteExpiration (the built-in in-memory implementation evicts expired entries on read).

It isn't covered by the three conformance suites. In a multi-replica deployment, you can map it directly to a key-value store with TTLs, such as Redis.

Released under the Apache License 2.0 · Documentation © 2026 Wicket authors