Help us shape the future of Go! Take the 2026 Go Developer Survey

fcsdk

package module
v0.0.13 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 0 Imported by: 0

README

FlowCatalyst Go SDK

The Go member of the FlowCatalyst SDK family (TypeScript, Laravel, Java and Rust live beside it under clients/). It is a separate Go module:

github.com/flowcatalyst/go-sdk

It is also the dependency of the FlowCatalyst application framework's FlowCatalyst module (../flowcatalyst-framework): the framework builds on this module rather than talking to the platform itself.

What it gives an application:

  • use cases and an outbox: one business operation writes its aggregate, its domain event and its audit row in one transaction, into outbox_messages; the platform's outbox processor delivers them;
  • dispatch jobs queued in the same transaction;
  • declarative sync of everything an application owns on the platform (roles, event types, dispatch pools, connections, subscriptions, processes, scheduled jobs, principals, its OpenAPI document and its documentation pages);
  • a platform API client, OIDC/OAuth helpers, webhook signature validation, a scheduled-job runner, TSIDs, and pluggable cache and lock providers.

Install

Go 1.26 or later.

go get github.com/flowcatalyst/go-sdk@latest

Releases are the tags vX.Y.Z on github.com/flowcatalyst/go-sdk, one commit per release. To build against a local checkout instead:

go mod edit -require=github.com/flowcatalyst/go-sdk@v0.0.0-00010101000000-000000000000 \
            -replace=github.com/flowcatalyst/go-sdk=../go-sdk

Versions up to v0.0.8 were published as github.com/flowcatalyst/flowcatalyst/pkg/fcsdk; moving to this module path means changing the import paths (github.com/flowcatalyst/flowcatalyst/pkg/fcsdk/... becomes github.com/flowcatalyst/go-sdk/...). Nothing else changed.

A use case, end to end

examples/order-service is the complete, runnable version of this section (its test runs it against Postgres when FC_TEST_DATABASE_URL is set).

HTTP request → command
             → usecase.ExecutionContext (principal, correlation, execution ids)
             → usecaseop.Run(ctx, uow, PlaceOrder(repo), cmd, ec)
                  Validate → Authorize → Execute → Plan
                  the Plan is applied in ONE transaction:
                    repository Persist      → orders
                    Sink.WriteEvent         → outbox_messages (EVENT)
                    Sink.WriteDispatchJobs  → outbox_messages (DISPATCH_JOB, if the plan has jobs)
                    Sink.WriteAudit         → outbox_messages (AUDIT_LOG)
             → (event, error) → HTTP 201 / 4xx / 500

An operation is a value with named phases; Execute can reach the database only by returning a Plan (usecaseop.Save, Delete, Emit, SaveAll, Sync, plus .Dispatch(jobs...) for dispatch jobs), and usecaseop.Run is the only thing that applies one, so "row written ⇒ event, jobs and audit written, atomically" holds by construction.

func PlaceOrder(repo OrderRepository) usecaseop.Operation[PlaceOrderCommand, OrderPlaced] {
	return usecaseop.Operation[PlaceOrderCommand, OrderPlaced]{
		Name: "PlaceOrder",
		Validate: func(_ context.Context, cmd PlaceOrderCommand) error {
			if cmd.TotalCents <= 0 {
				return usecase.Validation("TOTAL_INVALID", "totalCents must be positive")
			}
			return nil
		},
		Authorize: usecaseop.Public[PlaceOrderCommand], // or a real check
		Execute: func(_ context.Context, cmd PlaceOrderCommand, ec usecase.ExecutionContext) (usecaseop.Plan[OrderPlaced], error) {
			order := &Order{ID: tsid.GenerateWithPrefix("ord"), CustomerID: cmd.CustomerID, TotalCents: cmd.TotalCents}
			event := OrderPlaced{Meta: newMeta(ec, "sales", "order", "placed", order.ID), OrderID: order.ID}
			return usecaseop.Save(order, repo, event), nil
		},
	}
}

// wiring
sink := outboxpgx.NewSink(outboxpgx.Config{
	ClientCode:      "acme",   // the client's identifier slug (optional)
	ApplicationCode: "orders", // required on audit rows for an application-scoped credential
	AuditEnabled:    true,
})
uow := usecasepgx.New(pool, sink)

// in the handler
event, err := usecaseop.Run(r.Context(), uow, PlaceOrder(repo), cmd, usecase.NewExecutionContext(principalID))

The repository implements usecasepgx.Persist[A] (aggregates never persist themselves); the event implements usecase.DomainEvent (usually by embedding usecase.EventMetadata). A command may implement AuditOperation() string to name its audit row's operation and AuditData() any to choose what the row records; whatever is recorded is redacted first (usecase.RedactAuditJSON: passwords, secrets, tokens, API keys...).

The outbox rows

Package outbox builds every row; outboxpgx (pgx, the use-case Sink) and outboxsql (database/sql, Postgres or MySQL) write them. The outbox processor sends each row's payload unchanged:

Row Endpoint Payload
EVENT POST /api/events/batch specVersion, type, source, subject, data, correlationId, causationId, deduplicationId, messageGroup, clientCode, contextData
AUDIT_LOG POST /api/audit-logs/batch entityType, entityId, operation, operationData, principalId, performedAt, source, correlationId, applicationCode, clientCode
DISPATCH_JOB POST /api/dispatch-jobs/batch the batch item (id, kind, code, source, targetUrl, payload, ...)
  • data and operationData are the JSON value itself, never a string of encoded JSON (the other SDKs encode them; the platform unwraps those).
  • The event row's id and deduplicationId are the event id.
  • clientCode (the client's identifier slug) comes from the writer's configuration or from an event implementing outbox.ClientCoded. Leave it empty to let the platform scope by the credential: an anchor writes platform-scoped events, a single-client credential writes under its client; a code the credential cannot reach is refused (403, whole batch).
  • An event without source gets the platform's default, {application}:{subdomain} of its type.

The rows, the INSERT statement and the dispatch-job rules are the application framework's outbox conformance vector, pinned by outbox/conformance_test.go (outbox/testdata/conformance).

Dispatch jobs

A use case declares dispatch jobs on its plan, next to the aggregate write and the event (framework write-path contract 7a; examples/dispatch-jobs):

Execute: func(_ context.Context, cmd IssueInvoiceCommand, ec usecase.ExecutionContext) (usecaseop.Plan[InvoiceIssued], error) {
	inv, event := /* ... */
	export, err := outbox.NewDispatchJob("billing:ledger:export", "https://ledger.example.com/exports").
		WithEventID(event.EventID()).
		WithJSONPayload(map[string]any{"invoiceId": inv.ID})
	if err != nil {
		return nil, err
	}
	notify := outbox.NewDispatchJob("billing:invoice:notify", cmd.NotifyURL).
		WithClientCode(cmd.ClientCode). // or WithPlatformClientID; the platform resolves the code
		WithMessageGroup("invoice:" + inv.ID)
	return usecaseop.Save(inv, repo, event).Dispatch(export, notify), nil
},

usecaseop.Run writes, in ONE transaction: the aggregate, the event, the jobs (in the order the plan lists them), the audit row. If anything fails, nothing is written.

  • Every job is validated before the transaction opens (code {application}:{rest}, absolute http(s) target with no user info, ids [0-9A-Za-z_-]{1,17}, ...): a row the platform would refuse terminally is never stored. An invalid job fails the operation as internal DISPATCH_JOB_INVALID whose message is the rule's text; check values that come from the command in Validate (job.Validate()) to answer 400.
  • Defaults: kind TASK, source the code's application, payload {} as application/json, a generated 13-character id (in the payload, so a redelivered batch creates the job with the same id), and the plan's correlation id (its event's; a Sync plan's rollup) unless the job sets one. The causation id is never sent (the batch endpoint has no such field), and eventId only when the job sets it.
  • No client unless the job names one: the sink's ClientCode is not inherited. WithClientCode (resolved by the platform; an unknown code refuses the whole batch) or WithPlatformClientID, not both. Without either the job is platform-scoped and uses the default pool.
  • Dispatch returns a new plan; calls accumulate. A plan always has its event, so a job never travels alone.
  • The sink must implement usecasepgx.DispatchJobSink (outboxpgx.Sink does); otherwise DISPATCH_JOB_UNSUPPORTED, again before any write.

Lower-level paths, for code outside the usecaseop envelope:

// a scoped commit inside usecasepgx.RunErr: same order, same checks
usecasepgx.CommitScoped(ctx, s, inv, repo, event, cmd, usecasepgx.WithDispatchJobs(job))

// a transaction the application runs itself: just the row
err = sink.WriteDispatchJob(ctx, tx, job) // tx is a pgx.Tx

Sink.WriteDispatchJob (and outboxsql.Writer.WriteDispatchJob) validate and write the row only: no ordering with an event, no correlation id from anywhere, and the transaction is the caller's.

database/sql and MySQL
w, err := outboxsql.NewWriter(outboxsql.DialectMySQL, outboxsql.WriterConfig{ClientCode: "acme"})
// inside the application's own *sql.Tx:
err = w.WriteEvent(ctx, tx, event)
err = w.WriteAudit(ctx, tx, event, command)
err = w.WriteDispatchJob(ctx, tx, job)

outboxsql.InitSchema / outboxpgx.InitSchema create the table (use your own migrations in production). See examples/outbox-sql.

Syncing definitions

set := sync.ForApplication("orders").
	AddRole(sync.MakeRole("admin").WithPermissions("orders:sales:order:manage")).
	AddEventType(sync.MakeEventType("orders:sales:order:placed", "Order placed")).
	AddDispatchPool(sync.MakeDispatchPool("orders-default", "Orders").WithConcurrency(8).WithRateLimit(600)).
	AddConnection(sync.MakeConnection("orders-billing", "Billing webhooks")).
	AddSubscription(sync.MakeSubscription("order-billing", "Billing", "https://billing.example.com/webhooks").
		WithConnectionCode("orders-billing").
		Bind("orders:sales:order:placed", "")).
	AddProcess(sync.MakeProcess("orders:sales:place-order", "Place order").WithBody("graph LR; A-->B")).
	AddScheduledJob(sync.MakeScheduledJob("orders:nightly", "Nightly", "0 0 2 * * *").
		WithTargetURL("https://orders.example.com/jobs")).
	AddPrincipal(sync.MakePrincipal("ops@example.com").WithName("Ops").WithRoles("orders:admin")).
	AddDoc(sync.MakeDoc("getting-started", "# Getting started\n"))

res := sync.NewSynchronizer(c).Sync(ctx, set, sync.DefaultOptions())
if res.HasErrors() { log.Println(res.Errors()) }
log.Println("unknown roles skipped:", res.Principals.SkippedRoles)

One call per category, in dependency order (roles, event types, dispatch pools, connections, subscriptions, processes, scheduled jobs, principals, OpenAPI, docs); a failed category does not stop the others, except that subscriptions are not sent when connections failed. sync.WithRemoveUnlisted() removes the SDK-sourced rows a set no longer lists (never UI-created ones); docs are always a full replace. set.ForClient("acme") scopes connections and subscriptions to a client; scheduled jobs carry their own ForClient. SyncUsers syncs users without an application. See examples/fc-sync.

Platform client and auth

cc := auth.NewClientCredentialsProvider(auth.ClientCredentialsConfig{
	IssuerURL: "https://auth.flowcatalyst.io", ClientID: "svc-app", ClientSecret: secret,
})
c := client.New("https://api.flowcatalyst.io", client.WithTokenProvider(cc.Token))
types, err := c.EventTypes().List(ctx, &client.EventTypeFilters{Application: "orders"})

client retries transient 5xx and returns *client.APIError for non-2xx. auth.NewTokenValidator validates incoming RS256 bearer tokens through OIDC discovery and a cached JWKS; auth.OAuthClient does PKCE, refresh, revoke, introspect, userinfo and RP-initiated logout. See examples/list-event-types.

Every client method follows the platform's OpenAPI lock: a route that answers 204 No Content (update, suspend, activate, archive, delete, pause, resume, enable, disable) returns only an error; a create route that answers { "id" } returns *client.CreatedResponse (call Get for the record); and a route that answers a body returns it. go test ./client/ checks all of this against api/openapi.lock.json.

Ingestions

c.Ingestions() covers /api/ingestions, source kinds and connection tests (List, Create, Get, Update, Delete, Pause, Resume, ListVersions, SaveVersion, MakeVersionLive, Definition, History, Failures, DismissFailure, Secrets, SourceKinds, TestConnection, GetTest); c.IngestionSets() covers /api/ingestion-sets (List, Apply, ListApplies, GetApply, Reapply, Release; dryRun is a *bool, sent only when given). An ingestion needs an Application code (an INTEGRATION application) and a Client (id or identifier). The source and split are typed unions (client.HTTPSource, client.CSVSplit, ...); client.CustomSource{Type, Settings} is the escape hatch for a runtime extension's kind, and a kind this SDK does not know reads back as client.Unrecognized and is written back unchanged. Secrets are client.SecretRef names, never values. Lists are cursor-paged: pass the answer's NextCursor as After. Definition returns the YAML that fcdev ingest apply reads. The runtime API (/api/ingestion-runtime/v1/*) is the runtime's own client and is not in this SDK.

Without the promote permission Create, Update and SaveVersion save the definition as a draft, never a refusal: the answer's Save.Outcome is DRAFT and Save.AwaitingPromote is true until someone who holds promote makes it live (MakeVersionLive). A definition an existing version already says is not saved again (UNCHANGED, MADE_LIVE, DRAFT_EXISTS); versions carry Draft and Digest, and Warnings names what will not run as meant. Notices are failures the platform refused (Category rejected); an ingestion counts the open ones (Notices, LatestNotice). Failures(ctx, id, client.IngestionFailureFilters{Category: "rejected", Undismissed: true}) lists them and DismissFailure(ctx, id, failureID) dismisses one (it stays listed). Secrets(ctx, id) lists the secret names the live definition binds and each runtime's last fetch of them; never a value.

Platform config and secrets

c.PlatformConfig() has Get, Set, Delete and RevealSecret. Ordinary reads (Get, and the response to Set) return the literal "***" (client.SecretMask) as the value of every SECRET property, for every caller. The only way to read the plaintext is:

s, err := c.PlatformConfig().RevealSecret(ctx, "orders", "smtp", "password", "" /* clientID, optional */)

It always needs a reveal permission, never implied by being the application's service account: platform:admin:config:reveal (a platform administrator) or platform:application-service:config:reveal (an application's service account; the platform:application-service role carries it). The platform confines an application's service account to its own applications and to the clients it is linked to (its own client list, else the clients its application is enabled for), plus the application-wide value. Every reveal is audited by the platform. Writing "***" or an empty value to a SECRET is refused (400 SECRET_VALUE_MASKED / SECRET_VALUE_REQUIRED): to keep a secret, do not set it. Errors are *client.APIError: 403 not authorised, 404 no such property, 400 NOT_A_SECRET for a PLAIN property (use Get). The SDK does not cache the value and does not log it; SecretResponse redacts the value under %v/%+v/%#v and slog.

A service account acting for a person can have the platform's audit row name them: call Set, Delete or RevealSecret with client.WithOnBehalfOf(ctx, personID) (sent as X-On-Behalf-Of, at most 256 characters, no control characters; the platform honours it only from service accounts).

Field workers

The field-worker identity admin API (workers, devices, per-client policy and token introspection) is available as c.FieldWorkers(), c.FieldDevices(), c.FieldPolicies() and c.FieldTokens():

w, _ := c.FieldWorkers().Ensure(ctx, &client.EnsureFieldWorkerRequest{
    ClientID: "clt_1", WorkerCode: "driver-42", Name: "Sam", AppCodes: []string{"deliveries"},
})
pin, _ := c.FieldWorkers().ResetPin(ctx, w.ID, nil) // nil: platform generates a PIN
if pin.TemporaryPin != nil { /* shown once, hand it to the worker */ }

code, _ := c.FieldDevices().CreateEnrolmentCode(ctx, &client.CreateEnrolmentCodeRequest{
    ClientID: "clt_1", Mode: "SHARED", AppCodes: []string{"deliveries"},
}) // render code.QrPayload as a QR code

res, _ := c.FieldTokens().Introspect(ctx, accessToken) // res.Active

Workers: Ensure, List, Get, Update, Delete, ResetPin, Unlock, Suspend, Activate, GrantApp, RevokeApp, SignIns. Devices: CreateEnrolmentCode, List, Get, Suspend, Activate, Revoke. Policies: Get, Put. Timestamps are RFC 3339 strings; the action methods return only an error (the platform's { message } is discarded). Offline verification of a field access token is auth.FieldTokenVerifier; Introspect is the online check that the session is still live.

Packages

Package Purpose
usecase DomainEvent, EventMetadata, ExecutionContext, typed *Error, the sealed Result[E], audit redaction. No I/O.
usecaseop The use-case envelope: Operation[C,E] (Validate / Authorize / Execute → Plan, with Plan.Dispatch for dispatch jobs), Run, TxOperation / RunTx for multi-aggregate operations.
usecasepgx pgx unit of work: New, Persist[A], Sink / DispatchJobSink, Commit* / EmitEvent (with WithDispatchJobs), Run / RunErr with TxScopedUnitOfWork.
outbox The outbox rows (event, audit, dispatch job), the INSERT, DispatchJob and its validation. No I/O.
outboxpgx The use-case Sink (a DispatchJobSink) writing to outbox_messages via pgx, plus the lower-level WriteDispatchJob.
outboxsql Writer for database/sql (Postgres, MySQL) and the table DDL.
sync DefinitionSet + Synchronizer: declarative sync of every application category.
client Platform HTTP API client: event types, subscriptions, dispatch pools, applications, processes, principals, roles, permissions, audit logs, clients, connections, ingestions and ingestion sets, field workers/devices/policies/tokens, me, router, scheduled jobs, OpenAPI, docs.
auth Access-token claims, TokenValidator (JWKS), HmacTokenValidator, OAuthClient, ClientCredentialsProvider.
webhook HMAC-SHA256 webhook signature verification (Verifier for the platform's router; Validator for the SDK-peer shape).
scheduledjobs Runner for platform-fired scheduled jobs: handlers by job code, a lock per firing, logs and completion reported back.
tsid 13-character Crockford Base32 TSIDs, typed prefixes.
cache Byte cache with required TTLs and JSON helpers; MemoryCache, cache/postgrescache, cache/rediscache.
lock Distributed locks; Memory, lock/postgreslock, lock/redislock.

Examples

All of them build with go build ./... (CI does this).

Example What
examples/order-service A use case through usecaseop.Run, the pgx outbox, a dispatch job on a plan.
examples/dispatch-jobs A plan that saves an aggregate and carries two dispatch jobs (Save(...).Dispatch(...)); validating command-derived job fields.
examples/outbox-sql The database/sql writer: event, audit row and dispatch job in one *sql.Tx.
examples/fc-sync Syncing every category of one application.
examples/list-event-types client with a static token or ClientCredentialsProvider.
examples/scheduled-jobs-runner scheduledjobs.Runner behind an HTTP handler.
examples/webhook-receiver Webhook signature verification and its error switch.

Developing the SDK

The SDK is developed in the FlowCatalyst platform repository (pkg/fcsdk) and published here by its release script; changes made directly in this repository are not carried back. From the platform repository's root: make test-go-sdk (build, vet, race tests; the tests that need a database live in the nested module pkg/fcsdk/tests, which boots its own embedded Postgres and keeps embedded-postgres out of the SDK's go.mod: make test-go-sdk-tests, or go test ./... in that directory), make lint-go-sdk, make go-sdk-smoke (a scratch module consumes the SDK through a local replace and through go get of HEAD published to a throwaway repository), and RELEASE_DRY_RUN=1 make release-go-sdk BUMP=0.1.0. The platform module uses this tree through replace ... => ./pkg/fcsdk. The SDK imports nothing from the platform. Wire references: docs/wire-contract.md, the OpenAPI lock api/openapi.lock.json.

Unreleased

Behaviour changes (next release):

  • FieldTokenVerifier requires exp: a field token with no exp claim is refused as invalid_token (shared vector missing_exp).
  • auth.FieldMiddleware requires FieldTokenVerifierConfig.BaseURL and panics at construction without it. The proof's htu is compared with BaseURL + request path; the Host and X-Forwarded-Proto fallback is gone.
  • TokenValidator accepts token_use identity again (allowlist is api, identity or absent; field and anything else stay refused). v0.0.12's refusal of identity was an unintended break.

Changes before 0.1.0

The platform client was audited method by method against the OpenAPI lock. These signatures changed:

  • 204 routes return only error (they decoded an empty body into a struct before): Applications.Update, Applications.EnableForClient, Applications.DisableForClient, Clients.Update, Clients.EnableApplication, Clients.DisableApplication, Clients.UpdateApplications, DispatchPools.Update/Archive/Suspend/Activate, EventTypes.Update, Subscriptions.Update, Processes.Update, ScheduledJobs.Update/Pause/Resume/Archive/LogForInstance/CompleteInstance.
  • {id} creates return *CreatedResponse, not a fabricated aggregate: DispatchPools.Create, EventTypes.Create, Subscriptions.Create, Processes.Create. (Connections.Create is the opposite: the platform answers 201 with the whole connection, so it returns *ConnectionResponse.)
  • Responses that were discarded are returned: Connections.Pause/Activate (*ConnectionResponse), Principals.AddRole/RemoveRole (*PrincipalResponse), Principals.SetRoles (*SetRolesResponse), Principals.GrantClientAccess (*ClientAccessGrantResponse). Principals.SyncUsers returns *SyncUsersResult (syncedEmails, not syncedCodes). Roles.ListForApplication returns []RoleResponse (the platform answers a bare array). ScheduledJobs.Fire also carries id and scheduledJobId.
  • Routes the platform does not have, removed: Applications.GetServiceAccount (GET /api/applications/{id}/service-account does not exist; read the account with the new ServiceAccounts().Get, and point an application at one with the new Applications.LinkServiceAccount) and Applications.UpdateClientConfig (PUT .../clients/{clientId} does not exist; use Applications.GetClientConfig, EnableForClient and DisableForClient). ClientConfigRequest and SuccessResponse are gone.
  • Query parameters match the lock: Applications.List(ctx, appType, active) (no paging), Clients.List(ctx) (no filters or paging), Connections.List(ctx, clientID, status), EventTypes.List(ctx, *EventTypeFilters) (adds subdomain and aggregate), Processes.List(ctx, *ProcessFilters) (application, subdomain, status; clientId was never accepted), PrincipalFilters gains roles, paging and sorting, and InstanceFilters drops TriggerKind, From and To.
  • Wrong routes fixed: Processes.Archive called the hard DELETE; it now posts /archive, and Processes.Delete no longer sends a ?hard=true the platform ignores. DispatchPools.Archive called DELETE (a real delete); it now posts /archive, and DispatchPools.Delete is new.
  • Required request members are always sent: UpdateConnectionRequest.Name, UpdateEventTypeRequest.Name and AddSchemaVersionRequest.Version are required fields; CreateRoleRequest.ClientManaged and CreateScheduledJobRequest.Concurrent/TracksCompletion are sent when false. SyncEventTypesRequest takes []SyncEventTypeItem (code, name, description: the sync schema has no schema or client members).

Errors

See ERRORS.md for which sentinels use errors.Is and which typed errors use errors.As, per package.

License

Copyright © 2026 Belac (FlowCatalyst)

Licensed under the Apache License, Version 2.0 — see LICENSE and NOTICE in this directory (the module ships its own copies; a module download contains only this directory).

Documentation

Overview

Package fcsdk is the entry-point overview for the FlowCatalyst Go SDK, module github.com/flowcatalyst/go-sdk. There is no Go code in the root; this file exists so that `go doc` on the module path returns a map of what's where. It is also the dependency of the FlowCatalyst application framework's FlowCatalyst module.

Wire parity: TSIDs, webhook signatures and audit redaction match the other SDKs byte for byte (shared vectors). Outbox rows are the application framework's conformance vector under its comparison rule; the one deliberate difference is that an event's data and an audit row's operationData are written as the JSON value itself, never as encoded JSON in a string (see package outbox).

Mental model

A FlowCatalyst consumer app runs each business operation through the use-case envelope. The operation's Plan is applied in one transaction: the aggregate row, the domain event and (if enabled) the audit row; the event and audit rows land in outbox_messages, and the platform's outbox processor forwards them to /api/events/batch and /api/audit-logs/batch. Dispatch jobs written in the same transaction go to /api/dispatch-jobs/batch. Operations never call the platform directly during a transaction.

HTTP request → command DTO
             → usecase.ExecutionContext (principal + correlation)
             → usecaseop.Run(ctx, uow, op, cmd, ec)
                  ↓ Validate / Authorize / Execute → Plan
             → the Plan is applied in a single transaction
                  ↳ <Repo>.Persist
                  ↳ Sink.WriteEvent → outbox_messages
                  ↳ Sink.WriteAudit → outbox_messages (optional)
             → committed event → (E, error)
             → HTTP 201 / 4xx / 500

The Sink slot is what makes the SDK reusable. Consumer apps wire outboxpgx.Sink (writes to outbox_messages); the platform itself wires its own sink that writes directly to msg_events.

Package map

Domain primitives (no I/O):

  • usecase — DomainEvent + EventMetadata + ExecutionContext + typed Error, audit redaction, and the sealed Result[E]: Success requires a sealed.Token only pkg/fcsdk packages can mint.
  • usecaseop — the enforced use-case envelope: Operation[C,E] {Validate, Authorize, Execute→Plan} + Run, plus TxOperation / RunTx for multi-aggregate ops. An Execute can reach the DB only by returning a Plan that Run applies atomically.
  • outbox — the outbox rows (EVENT, AUDIT_LOG, DISPATCH_JOB), the INSERT statement and DispatchJob, held to the framework's conformance vector.
  • tsid — Time-Sorted IDs (Crockford Base32), typed prefixes and GenerateWithPrefix for app-specific IDs.

UnitOfWork driver:

  • usecasepgx — pgx-backed UoW. Entry points: Commit / CommitDelete / CommitAll / EmitEvent / CommitSync / Run / RunErr. The usecaseop envelope (Operation/Plan/Run) is built on top of it.

Outbox writers:

  • outboxpgx — the usecasepgx Sink, writing to outbox_messages via pgx; WriteDispatchJob for dispatch jobs.
  • outboxsql — Writer for database/sql (Postgres, MySQL) and the table DDL (InitSchema).

HTTP I/O:

  • client — *FlowCatalystClient + resource families (event types, subscriptions, dispatch pools, applications, processes, principals, roles, permissions, audit logs, clients, connections, field workers, devices, policies and tokens, me, router, scheduled jobs, openapi, docs). Retry on transient 5xx, typed *APIError. Bearer token or TokenProvider auth.
  • auth — AccessTokenClaims + AuthContext; TokenValidator (RS256 via JWKS auto-discovery through lestrrat-go/jwx/v2); HmacTokenValidator (HS256); OAuthClient (PKCE auth-code, refresh, revoke, introspect, userinfo, RP-initiated logout); ClientCredentialsProvider (satisfies client.TokenProvider).
  • webhook — Two HMAC-SHA256 validators. Verifier matches the platform's router (uppercase headers, ISO8601 timestamps); Validator matches the SDK-peer shape (mixed-case headers, Unix-second timestamps).
  • sync — DefinitionSet + Synchronizer for declarative reconciliation of roles, event types, dispatch pools, connections, subscriptions, processes, scheduled jobs, principals, the OpenAPI document and documentation pages. One call per category.
  • scheduledjobs — consumer-side Runner. Register HandlerFuncs by job code; serialises via a lock.Provider, streams logs back, reports completion.

Infrastructure:

  • cache — pluggable byte-oriented Cache + Get/Set/GetOrSet JSON helpers. MemoryCache ships here; cache/postgrescache and cache/rediscache are opt-in sub-packages.
  • lock — distributed-lock Provider + Handle. Memory ships here; lock/postgreslock and lock/redislock are opt-in sub-packages.

Internal:

  • internal/sealed — Token type that gates usecase.Success. Constructable only by packages of this module (Go's internal rule), which scopes the seal to the SDK.

License

Copyright 2026 Belac (FlowCatalyst). Licensed under the Apache License, Version 2.0 (see the LICENSE and NOTICE files in this directory), the same licence as the rest of the repository.

Directories

Path Synopsis
Package auth provides authentication primitives for FlowCatalyst SDK applications: JWT validation (RS256 via JWKS, or HS256 via a shared secret), the OAuth2 authorization-code flow with PKCE, and a client_credentials TokenProvider that plugs into client.WithTokenProvider.
Package auth provides authentication primitives for FlowCatalyst SDK applications: JWT validation (RS256 via JWKS, or HS256 via a shared secret), the OAuth2 authorization-code flow with PKCE, and a client_credentials TokenProvider that plugs into client.WithTokenProvider.
Package cache is a pluggable key-value cache with required TTL on every write.
Package cache is a pluggable key-value cache with required TTL on every write.
postgrescache
Package postgrescache is a pgx-backed cache that satisfies cache.Cache.
Package postgrescache is a pgx-backed cache that satisfies cache.Cache.
rediscache
Package rediscache is a Redis-backed cache that satisfies cache.Cache.
Package rediscache is a Redis-backed cache that satisfies cache.Cache.
Package client is the FlowCatalyst platform HTTP API client.
Package client is the FlowCatalyst platform HTTP API client.
examples
dispatch-jobs command
Command dispatch-jobs shows dispatch jobs declared on a use case's plan: one operation (IssueInvoice) saves an invoice and returns usecaseop.Save(...).Dispatch(...), so the invoice row, its event, two dispatch jobs and the audit row commit in ONE transaction, or none of them does.
Command dispatch-jobs shows dispatch jobs declared on a use case's plan: one operation (IssueInvoice) saves an invoice and returns usecaseop.Save(...).Dispatch(...), so the invoice row, its event, two dispatch jobs and the audit row commit in ONE transaction, or none of them does.
fc-sync command
Command fc-sync is the declarative-reconciliation example.
Command fc-sync is the declarative-reconciliation example.
list-event-types command
Command list-event-types is the smallest possible FlowCatalyst SDK example.
Command list-event-types is the smallest possible FlowCatalyst SDK example.
order-service command
Command order-service is the end-to-end example of a FlowCatalyst consumer application: one business operation (PlaceOrder) through the use-case envelope, its aggregate, domain event and audit row written to the outbox in one transaction, and a second operation whose plan carries a dispatch job, written in the same transaction as the event that asks for it.
Command order-service is the end-to-end example of a FlowCatalyst consumer application: one business operation (PlaceOrder) through the use-case envelope, its aggregate, domain event and audit row written to the outbox in one transaction, and a second operation whose plan carries a dispatch job, written in the same transaction as the event that asks for it.
outbox-sql command
Command outbox-sql writes outbox rows through database/sql, for an application that does not use pgx (or runs on MySQL): an event, its audit row and a dispatch job, committed with the application's own write in one *sql.Tx.
Command outbox-sql writes outbox rows through database/sql, for an application that does not use pgx (or runs on MySQL): an event, its audit row and a dispatch job, committed with the application's own write in one *sql.Tx.
scheduled-jobs-runner command
Command scheduled-jobs-runner mounts a scheduledjobs.Runner behind a stdlib HTTP handler.
Command scheduled-jobs-runner mounts a scheduledjobs.Runner behind a stdlib HTTP handler.
webhook-receiver command
Command webhook-receiver shows how to mount the webhook validator behind a stdlib HTTP handler.
Command webhook-receiver shows how to mount the webhook validator behind a stdlib HTTP handler.
internal
sealed
Package sealed provides a compile-time token that gates construction of [usecase.Result] success values.
Package sealed provides a compile-time token that gates construction of [usecase.Result] success values.
Package lock is a pluggable distributed-lock contract.
Package lock is a pluggable distributed-lock contract.
postgreslock
Package postgreslock is a table-based distributed lock that satisfies lock.Provider.
Package postgreslock is a table-based distributed lock that satisfies lock.Provider.
redislock
Package redislock is a Redis-backed distributed lock that satisfies lock.Provider.
Package redislock is a Redis-backed distributed lock that satisfies lock.Provider.
Package outbox builds the rows the FlowCatalyst outbox writers store in outbox_messages: the row columns, the INSERT statement, and the payload of each row type (EVENT, AUDIT_LOG, DISPATCH_JOB).
Package outbox builds the rows the FlowCatalyst outbox writers store in outbox_messages: the row columns, the INSERT statement, and the payload of each row type (EVENT, AUDIT_LOG, DISPATCH_JOB).
Package outboxpgx implements the consumer-facing Sink for use cases running against pgx.
Package outboxpgx implements the consumer-facing Sink for use cases running against pgx.
Package outboxsql is the outbox for consumer apps running against database/sql, for both Postgres (any driver: lib/pq, pgx/stdlib, etc.) and MySQL (go-sql-driver/mysql).
Package outboxsql is the outbox for consumer apps running against database/sql, for both Postgres (any driver: lib/pq, pgx/stdlib, etc.) and MySQL (go-sql-driver/mysql).
Package scheduledjobs implements the consumer-side dispatcher for platform-fired scheduled-job webhooks.
Package scheduledjobs implements the consumer-side dispatcher for platform-fired scheduled-job webhooks.
Package sync orchestrates declarative reconciliation of an application's FlowCatalyst definitions against the platform.
Package sync orchestrates declarative reconciliation of an application's FlowCatalyst definitions against the platform.
Package tsid generates Time-Sorted IDs as Crockford Base32 strings.
Package tsid generates Time-Sorted IDs as Crockford Base32 strings.
Package usecase implements the sealed Result pattern used by the FlowCatalyst SDK and the platform itself, plus the DomainEvent / ExecutionContext / typed Error primitives the use-case envelope builds on.
Package usecase implements the sealed Result pattern used by the FlowCatalyst SDK and the platform itself, plus the DomainEvent / ExecutionContext / typed Error primitives the use-case envelope builds on.
Package usecaseop is the enforced use-case envelope for FlowCatalyst business operations.
Package usecaseop is the enforced use-case envelope for FlowCatalyst business operations.
Package usecasepgx is the pgx-backed UnitOfWork for FlowCatalyst use cases.
Package usecasepgx is the pgx-backed UnitOfWork for FlowCatalyst use cases.
Package webhook implements webhook signature verification for SDK consumers receiving FlowCatalyst webhooks.
Package webhook implements webhook signature verification for SDK consumers receiving FlowCatalyst webhooks.

Jump to

Keyboard shortcuts

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