gist-server documentation

This is the technical documentation of every service supported by gist-server.

The running example throughout is orderflow, a small order-processing backend — Order and OrderItem rows in MySQL, a payment-gateway call, order search, receipt storage, a lifecycle state machine, a nightly reconciliation job, and an event bus. It exists purely to give every service a concrete call site.

Prerequisites:

At a glance — what each service exposes to your own Go code
#ServiceSurface
LoggingAlways on
1 gist-api-server API
2gist-authNo API
3 gist-aws-s3-client API
4 gist-aws-ses-client API
5 gist-edge-proxy Config only
6 gist-elasticsearch-client API
7 gist-fixtures API
8 gist-google-cloud-storage-client API
9 gist-http-client API
10 gist-http-server Config only
11 gist-mcp-bridge Config only
12 gist-mysql-client API
13 gist-parameter-store API
14 gist-postgres-client API
15 gist-pub-sub-client API
16 gist-push-client API
17 gist-rabbit-mq-client API
18 gist-redis-client API
19 gist-scheduler API
20 gist-state-machine API

API The service's own exposed API, i.e the Go functions/types your code calls directly (e.g. gist-mysql-client's Find/Save)

Config only No Go API — you point config at it and it runs on its own.

No API gist-server does something with it internally, but there's no injectable Service type or package your code can call at all.

Always on Not a configurable service — no config entry, no enabled flag, always running.

🔒 This field can hold a resolvable reference instead of a raw value — see the Secrets section.

Accessing a service

By grouping services you can create a collection of services tailored for the specific needs by your code.

Every example below assumes the same servicesGroup pattern — a plain struct whose fields are name-tagged to match a service's config id.

Go
type ServicesGroup struct {
    Logger          logging.Logger
    OrdersDB        *gistmysqlclient.Service `name:"orders-db"`
    OrdersIndex     *gistelasticsearchclient.Service `name:"order-es-client"`
    ReceiptsStorage *gistgooglecloudstorageclient.Service `name:"receipts-gcp-storage"`
    // ...one field per service instance you actually use
}

gist.NewApp builds one ServicesGroup (or several, if different handler groups need different subsets) via reflection over these tags, and hands it to every EndpointHandler, RegisterTriggerFunc, RegisterScheduleFunc, etc.

The examples below accept this type as a parameter. None of the examples redeclare this struct — assume sg ServicesGroup throughout.

gist-server command line

The gist-server binary has three independent modes, selected by which flags are set. Flag list verbatim from gist-server -h.

1. Normal run — what gist.NewApp invokes for you

Shell
gist-server -admin-socket <path> -callback-socket <path> -config <path>
FlagDescription
-admin-socket <path>Path to the admin gRPC unix socket. Your own process registers its endpoint/trigger/schedule handlers here.
-callback-socket <path>Path to the callback gRPC unix socket — the other half of the handshake; gist-server calls back into your process here.
-config <path>Path to your config.json.

You won't usually type this yourself: gist.NewApp reads config.json's own top-level "gist-binary" field for the binary path, generates both socket paths per run, and execs this exact command. Only reach for it directly when you're driving gist-server outside the intended way (debugging, a non-Go customer process), i.e., not using the Gist package.

2. Scaffold a new Go project

Shell
gist-server -new-project orderflow
gist-server -new-project orderflow -project-path ./somewhere/else
FlagDescription
-new-project <name>Generates a starting go.mod + main.go; <name> becomes the Go module name. Refuses to run if a go.mod already exists at the target path.
-project-path <dir>Where to scaffold it. Defaults to ./<name>.

3. Generate a starting config.json

Shell
gist-server -generate-config gist-api-server,gist-mysql-client -out config.json
gist-server -generate-config all -out config.json
gist-server -generate-config all -config-detail full -out config.json
FlagDescription
-generate-config <kinds>Comma-separated service kinds, or all.
-config-detail <minimal|full>Which of each requested kind's own fields to write out. Default minimal.
-out <path>Output path. Default config.json.

-config-detail minimal (the default) writes out only each requested kind's own required fields - no "default" tag, and not a pointer (the same fields docs.html's own Minimal tab shows for that kind) - a short, fill-in-the-blanks starting point instead of a wall of fields you'd otherwise have to read past. -config-detail full writes out every field of every requested kind's real Config struct instead, each with a placeholder value (its own real default when it has one) - useful when you want to see, and override, every knob a service exposes without cross-referencing the docs for each one.

4. Combining scaffold + config

Shell
gist-server -new-project orderflow -generate-config gist-api-server,gist-mysql-client

Scaffolds the project and writes config.json into it in one call. If -out was left at its bare default, the config lands at <project-path>/config/config.json. -config-detail applies here too - the generated config.json is minimal by default, same as calling -generate-config on its own.

Logging

Always on

Logging is a core feature of gist-server. It's shared across all services, and your own process can use it to log to the same place. Selects the backend via config.json's own top-level logger field:

Field Type Description
logger string Backend gist-server (and this SDK) log through: slog-text — stdlib log/slog, human-readable text lines; slog-json — stdlib log/slog, one JSON object per line; logrus — sirupsen/logrus, text formatter; zap — uber-go/zap, JSON output. (default: slog-text)
JSON
{
  "name": "orderflow",
  "gist-binary": "./bin/gist-server",
  "logger": "slog-json",
  "services": { }
}

Usage

Five package-level functions, one per level — Debug/Info/ Warn/Error/Panic — each (msg string, fields map[string]any). fields carries whatever structured context matters for that line, rather than string-formatting it into msg itself; pass nil when there's none. Panic sends its line first, then panics locally with msg, same as a bare panic() call.

Go
logging.Info("order approved", map[string]any{
    "order_id":    order.ID,
    "customer_id": order.CustomerID,
})
logging.Error("charge failed", map[string]any{
    "order_id": order.ID,
    "error":    err.Error(),
})
logging.Debug("cache miss", nil)

Or declare a Logger field on your own ServicesGroup — no name tag needed, unlike every other injected service, since there's exactly one logger per process:

Go
type ServicesGroup struct {
    Logger logging.Logger
    // ...
}

func (sg ServicesGroup) something() {
    sg.Logger.Warn("retrying payment", map[string]any{"attempt": 2})
}

Secrets

Any config.json field marked 🔒 elsewhere in these docs (a password, an API key, a signing secret) can hold a resolvable reference instead of its raw value, resolved once at startup before that field is ever used - the raw value is never stored in config.json at all. Three schemes:

SchemeResolves to
env://VAR_NAMEThe named environment variable's own value, read from the process's own environment. Errors at startup if unset.
gcp-sm://projects/P/secrets/S/versions/VA Google Cloud Secret Manager secret, fetched directly using the process's own ambient Application Default Credentials - independent of any configured service instance.
gist-ps://<instance-id>/<source>/<alias>An alias in a configured gist-parameter-store instance - source is plain, env, or gcp-sm, matching that instance's own three backends. The one place to update a value when several fields reference it.

A plain literal value still works too - nothing about this is required, and every existing config.json stays valid as-is. See gist-parameter-store's own Usage section for a full worked example of each scheme.

01

gist-api-server

Runs your public HTTP API. You declare each endpoint's routing and metadata in endpoints.json, then bind a Go handler to it at startup via Attach. An endpoint with no handler yet just answers 501 automatically, so you can plan routes before the code behind them exists. A full OpenAPI spec and docs UI are generated automatically from endpoints.json and your handlers' own input/output struct tags — nothing to hand-write or keep in sync.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
title string API title shown in the OpenAPI/docs schema.
description string API description shown in the OpenAPI/docs schema.
endpoints-file string Path to the endpoints JSON file (route/doc metadata) loaded into Endpoints.
JSON
"gist-api-server": [
  {
    "id": "customer-login",
    "enabled": true,
    "title": "orderflow API",
    "description": "Order processing API for orderflow.",
    "endpoints-file": "./config/endpoints.json"
  }
]

Go API

Go
package gistapiserver

Attach[servicesGroup any](serviceID string, handlers ...*Handler[servicesGroup]) gist.Option
// AttachMiddlewares wires one or more MiddlewareHandler declarations into server, the same way Attach wires EndpointHandler declarations - a separate function, not a parameter on Attach, so an existing Attach(serviceID, handlers...) call site never has to change shape to add a middleware alongside its handlers.
AttachMiddlewares[servicesGroup any](serviceID string, middlewares ...*Middleware[servicesGroup]) gist.Option
NewExpectedError(code status.Code, message string) ExpectedError
(e ExpectedError) Error() string
(e ExpectedError) StatusCode() int32
(e ExpectedError) WithMessage(message string, values ...any) ExpectedError
(e ExpectedError) WithoutMessage() ExpectedError
// EndpointHandler registers id's real handler, fn - see Attach.
EndpointHandler[servicesGroup any, in any, out any](
	id string,
	expectedErrors []ExpectedError,
	fn func(sg servicesGroup, ctx context.Context, in in) (out, error),
	mockData out,
) *Handler[servicesGroup]
// PaginatedEndpointHandler is EndpointHandler's counterpart for a list endpoint whose result set can be arbitrarily large - the customer's own fn receives already-resolved, already-clamped page/limit values (read from the request's own ?page=/?limit= query params, which this adds to the endpoint's schema automatically - no need to declare them on in) and is expected to push them down into its own query (e.g.
PaginatedEndpointHandler[servicesGroup any, in any, out any](
	id string,
	expectedErrors []ExpectedError,
	fn func(sg servicesGroup, ctx context.Context, in in, page, limit int) (items []out, total int64, err error),
	mockItems []out,
) *Handler[servicesGroup]
// RawEndpointHandler is EndpointHandler's counterpart for a response that isn't a JSON-encoded struct at all - an SVG diagram, a PDF, a plain image - anything whose bytes need to reach the caller exactly as the handler produced them, under a Content-Type other than "application/json".
RawEndpointHandler[servicesGroup any, in any](
	id string,
	expectedErrors []ExpectedError,
	contentType string,
	fn func(sg servicesGroup, ctx context.Context, in in) ([]byte, error),
) *Handler[servicesGroup]
// RawHTTPEndpointHandler is EndpointHandler's/RawEndpointHandler's counterpart for a handler that needs a genuine http.ResponseWriter/*http.Request pair - a real redirect, cookies, arbitrary headers, anything a plain net/http.HandlerFunc can do - instead of a JSON in/out contract or a fixed-status byte blob.
RawHTTPEndpointHandler[servicesGroup any](
	id string,
	fn func(sg servicesGroup, w http.ResponseWriter, r *http.Request),
) *Handler[servicesGroup]
// StreamEndpointHandler is EndpointHandler for an endpoint that answers with Server-Sent Events: fn gets its input like any endpoint, checks it (returning an ExpectedError before opening fails the request as usual), then writes events to stream until it returns or ctx is cancelled - the client disconnecting cancels it.
StreamEndpointHandler[servicesGroup any, in any](
	id string,
	expectedErrors []ExpectedError,
	fn func(sg servicesGroup, ctx context.Context, in in, stream *Stream) error,
) *Handler[servicesGroup]
// MiddlewareHandler declares one named middleware - name is whatever string an endpoint's (or an instance's own) config.json "middlewares" list references.
MiddlewareHandler[servicesGroup any](
	name string,
	fn func(sg servicesGroup, ctx context.Context, req *MiddlewareRequest) (*MiddlewareResponse, error),
) *Middleware[servicesGroup]
// Open accepts the request: the client gets 200 text/event-stream right away, before any event.
(s *Stream) Open() error
// Send writes one event: event names it (empty for the default "message" event) and data is JSON-encoded as its payload.
(s *Stream) Send(event string, data any) error
02

gist-auth

No API

Handles OAuth login against google, gitea, or apple, built directly on goth's own transport-agnostic Provider/Session interfaces rather than its http.ResponseWriter/*http.Request/cookie-session sugar layer - so gist-server needs no server-side session at all: CSRF protection is a signed, stateless state token instead (state-secret below, valid 10 minutes). Your own process calls two plain typed Go methods - Begin/Complete - from inside whichever HTTP endpoints it already runs, exactly like any other injected client service; see Wiring begin/callback below. Configure one instance per provider, since provider registration is shared across the whole process.

Providers

provider/callback-url/state-secret apply the same way regardless of provider; each provider's own client-id/scopes/etc. live in a nested block named after it (google/gitea/apple) - only the block matching provider needs to be set, the other two are ignored.

google

FieldBehavior
google.client-idOAuth client ID.
google.client-secretOAuth client secret issued by Google.
google.scopesOAuth scopes requested.
FieldTypeDescription
idstringUnique instance ID.
enabledboolIf false, this entry is skipped entirely.
providerstringgoogle.
callback-urlstringOAuth callback/redirect URL registered with the provider — wherever your own process mounts the handler that calls Complete.
state-secretstringKey signing the stateless OAuth state CSRF token.
google.client-idstringOAuth client ID.
google.client-secretstringOAuth client secret issued by Google.
google.scopes[]stringOAuth scopes requested.
JSON
"gist-auth": [
  {
    "id": "customer-login",
    "enabled": true,
    "provider": "google",
    "callback-url": "https://orderflow.example.com/auth/callback",
    "state-secret": "...",
    "google": {
      "client-id": "...",
      "client-secret": "...",
      "scopes": ["email", "profile"]
    }
  }
]

gitea

FieldBehavior
gitea.server-urlBase URL of your self-hosted Gitea instance - the authorize/token/profile endpoints are derived from this at Gitea's own fixed paths (/login/oauth/authorize, /login/oauth/access_token, /api/v1/user), not independently configurable.
gitea.client-secretOAuth client secret issued by that Gitea instance.
FieldTypeDescription
idstringUnique instance ID.
enabledboolIf false, this entry is skipped entirely.
providerstringgitea.
callback-urlstringOAuth callback/redirect URL registered with the provider — wherever your own process mounts the handler that calls Complete.
state-secretstringKey signing the stateless OAuth state CSRF token.
gitea.client-idstringOAuth client ID.
gitea.client-secretstringOAuth client secret issued by that Gitea instance.
gitea.server-urlstringBase URL of your self-hosted Gitea instance.
gitea.scopes[]stringOAuth scopes requested.
JSON
"gist-auth": [
  {
    "id": "customer-login",
    "enabled": true,
    "provider": "gitea",
    "callback-url": "https://orderflow.example.com/auth/callback",
    "state-secret": "...",
    "gitea": {
      "client-id": "...",
      "client-secret": "...",
      "server-url": "https://git.orderflow.example.com",
      "scopes": ["read:user"]
    }
  }
]

apple

FieldBehavior
apple.team-idApple Developer Team ID.
apple.key-idSign in with Apple key ID.
apple.private-keyApple private key (PEM) — derives the OAuth client secret directly.

apple.private-key generates the OAuth client secret for apple (no separate client-secret field - Apple never issues a static one).

FieldTypeDescription
idstringUnique instance ID.
enabledboolIf false, this entry is skipped entirely.
providerstringapple.
callback-urlstringOAuth callback/redirect URL registered with the provider — wherever your own process mounts the handler that calls Complete.
state-secretstringKey signing the stateless OAuth state CSRF token.
apple.client-idstringOAuth client ID (Services ID).
apple.team-idstringApple Developer Team ID.
apple.key-idstringSign in with Apple key ID.
apple.private-keystringApple private key (PEM) — derives the OAuth client secret.
apple.scopes[]stringOAuth scopes requested.
JSON
"gist-auth": [
  {
    "id": "customer-login",
    "enabled": true,
    "provider": "apple",
    "callback-url": "https://orderflow.example.com/auth/callback",
    "state-secret": "...",
    "apple": {
      "client-id": "...",
      "team-id": "...",
      "key-id": "...",
      "private-key": "-----BEGIN PRIVATE KEY-----...",
      "scopes": ["name", "email"]
    }
  }
]

Minting a token

Leave token.token-type empty (the default) and Complete just returns the raw OAuth profile - no token minted. Set it to match one of your gist-api-server instance's own auths catalog entries (same environment/issuer/audience/ signing-string/max-age) and Complete also mints a JWT that passes that endpoint's own auth check unchanged - no gist-api-server code change needed. subject picks which profile field becomes the JWT subject: user_id (default) or email.

Wiring begin/callback

gist-server can't serve customer-facing HTTP itself, so Begin/Complete live on the SDK's own gistauth.Service (injected into your ServicesGroup the same way every other client service is - a plain GoogleAuth *gistauth.Service field tagged name:"customer-login") as two plain typed methods, not a route gist-server owns or a raw http.ResponseWriter/*http.Request pair to forward - call them from inside whichever HTTP endpoint you already write, exactly like sg.OrdersDB.Find(...) or any other injected client service:

Go
type BeginAuthOutput struct {
    RedirectURL string `json:"redirect_url"`
}

var BeginGoogleAuth = gistapiserver.EndpointHandler("begin-google-auth",
    []gistapiserver.ExpectedError{gistapiserver.Internal},
    func(sg ServicesGroup, ctx context.Context, _ struct{}) (BeginAuthOutput, error) {
        redirectURL, err := sg.GoogleAuth.Begin(ctx)
        return BeginAuthOutput{RedirectURL: redirectURL}, err
    },
    BeginAuthOutput{RedirectURL: "https://accounts.google.com/o/oauth2/auth?..."},
)

type CompleteAuthInput struct {
    Code  string `json:"code"  query:"code"`
    State string `json:"state" query:"state"`
}
type CompleteAuthOutput struct {
    Token string `json:"token"`
}

var CompleteGoogleAuth = gistapiserver.EndpointHandler("complete-google-auth",
    []gistapiserver.ExpectedError{gistapiserver.Internal},
    func(sg ServicesGroup, ctx context.Context, in CompleteAuthInput) (CompleteAuthOutput, error) {
        result, err := sg.GoogleAuth.Complete(ctx, in.Code, in.State)
        if err != nil {
            return CompleteAuthOutput{}, err
        }
        return CompleteAuthOutput{Token: result.Token}, nil
    },
    CompleteAuthOutput{Token: "..."},
)

result (gistauth.CompleteResult) also carries Profile (the OAuth provider's user profile) and ProviderAccessToken/ProviderRefreshToken (the provider's own OAuth tokens - not the minted Token) - shape your own CompleteAuthOutput around whichever of those your endpoint should expose. Register begin-google-auth/complete-google-auth in endpoints.json exactly like any other endpoint.

Logout is best-effort provider-side token revocation (only Google supports it today - Gitea/Apple return a clear "not supported" error) - call sg.GoogleAuth.Logout(ctx, providerAccessToken) with the ProviderAccessToken a previous Complete call returned, the same way.

Point this instance's own callback-url at wherever you mounted the complete-google-auth handler above (register that same URL with the OAuth provider too, e.g. in the Google/Gitea/Apple developer console) - through gist-api-server, that's wherever endpoints.json mounts it, prefixed with /v{version} if this instance's own prepend-version-to-path is on.

03

gist-aws-s3-client

Create, read, update, and delete buckets and objects in Amazon S3, or a local emulator (MinIO/LocalStack) for testing. You refer to a bucket by a short name from your own config, not S3's real bucket name. That mapping happens on the server side, so the real name never has to appear in your code.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
JSON
"gist-aws-s3-client": [
  {
    "id": "customer-login",
    "enabled": true
  }
]

Go API

Go
package gistawss3client

NewService(server *gist.Server, serviceID string) *Service
// CreateBucket creates a new bucket - see gist-server's CreateBucket for the underlying call and its already-taken-name-is-not-an-error convention; created reports whether this call was the one that created it.
(s *Service) CreateBucket(ctx context.Context, bucketID, location string) (created bool, err error)
// DeleteBucket deletes the bucket - S3 requires it to be empty first (see gist-server's DeleteBucket).
(s *Service) DeleteBucket(ctx context.Context, bucketID string) (found bool, err error)
// DeleteObject deletes fileName from bucketID.
(s *Service) DeleteObject(ctx context.Context, bucketID, fileName string) (found bool, err error)
(s *Service) Exists(ctx context.Context, bucketID, fileName string) (bool, error)
// GetBucket retrieves the bucket's metadata, decoding it into out (a pointer, same convention json.Unmarshal uses) - a {"name":..., "tags": {...}} shaped document (S3 has no single "bucket resource" the way GCS does).
(s *Service) GetBucket(ctx context.Context, bucketID string, out any) (found bool, err error)
// GetObject downloads fileName's raw content from bucketID.
(s *Service) GetObject(ctx context.Context, bucketID, fileName string) (found bool, content []byte, err error)
(s *Service) Store(ctx context.Context, bucketID, path string, fileHeader *multipart.FileHeader) (*string, *string, error)
// UpdateBucket merges attrs' own "tags" field into the bucket's existing tag set - e.g.
(s *Service) UpdateBucket(ctx context.Context, bucketID string, attrs any) (found bool, err error)
// UpdateObjectMetadata merges metadata into fileName's custom key/value metadata.
(s *Service) UpdateObjectMetadata(ctx context.Context, bucketID, fileName string, metadata map[string]string) (found bool, updated map[string]string, err error)
04

gist-aws-ses-client

Send transactional email via Amazon SES, or a local emulator for testing, and manage verified sender identities. You refer to a sender by a short name from your own config, not the real SES-verified "From" address. That mapping happens on the server side, so the real address never has to appear in your code.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
JSON
"gist-aws-ses-client": [
  {
    "id": "customer-login",
    "enabled": true
  }
]

Go API

Go
package gistawssesclient

NewService(server *gist.Server, serviceID string) *Service
// CreateEmailIdentity begins verifying emailIdentity (an email address or domain) with SES - see gist-server's CreateEmailIdentity for the underlying call and its already-verified-is-not-an-error convention; created reports whether this call was the one that started verification.
(s *Service) CreateEmailIdentity(ctx context.Context, emailIdentity string) (created bool, err error)
// DeleteEmailIdentity removes emailIdentity from SES.
(s *Service) DeleteEmailIdentity(ctx context.Context, emailIdentity string) (found bool, err error)
// GetEmailIdentity retrieves emailIdentity's own verification status.
(s *Service) GetEmailIdentity(ctx context.Context, emailIdentity string) (found, verified bool, verificationStatus string, err error)
// SendEmail sends a simple (non-templated) email from senderID's own verified "From" address.
(s *Service) SendEmail(ctx context.Context, senderID string, to, cc, bcc, replyTo []string, subject, textBody, htmlBody string) (messageID string, err error)
05

gist-edge-proxy

gist-server's own network edge — terminates TLS in front of your HTTP(S) backends, round-robin load-balancing across health-checked servers per load balancer, with certificates issued and renewed automatically (self-signed locally, Let's Encrypt in staging/production). One instance can front several domains at once, each routed by hostname. It also forwards raw TCP traffic straight through to anything else reachable from gist-server via port-exposes, with no TLS or HTTP awareness. Use load-balancers for HTTP(S); port-exposes for everything else.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
acme ACMEConfig Certificate-issuance settings.
JSON
"gist-edge-proxy": [
  {
    "id": "customer-login",
    "enabled": true,
    "acme": {
      "ca": "production",
      "email": "ops@orderflow.example",
      "cert-storage-path": "./data/gist-edge-proxy/public-edge",
      "timeout": "60s"
    }
  }
]
06

gist-elasticsearch-client

Index and query one Elasticsearch cluster through seven simple methods. Every method fails the same consistent way, so error handling stays predictable across all of them. A missing document is never treated as an error — it just comes back as a plain bool instead.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
user string 🔒 Username for basic auth against the cluster.
password string 🔒 Password for basic auth against the cluster.
JSON
"gist-elasticsearch-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "user": "elastic",
    "password": "orderflow"
  }
]

Go API

Go
package gistelasticsearchclient

NewService(server *gist.Server, serviceID string) *Service
// BulkIndex indexes every item into index in one round trip.
(s *Service) BulkIndex(ctx context.Context, index string, items []BulkItem) (hasErrors bool, results []BulkResult, err error)
// CreateItem inserts item into index - never overwrites.
(s *Service) CreateItem(ctx context.Context, index, id string, item any) (created bool, actualID string, err error)
// DeleteItem deletes the document at id.
(s *Service) DeleteItem(ctx context.Context, index, id string) (found bool, err error)
// GetItem retrieves the document at id, decoding its stored source into out (a pointer, same convention json.Unmarshal uses).
(s *Service) GetItem(ctx context.Context, index, id string, out any) (found bool, err error)
// IndexItem creates or replaces the document at id - a full create-or-replace, unlike CreateItem's insert-only semantics.
(s *Service) IndexItem(ctx context.Context, index, id string, item any) error
// Search runs query - a full Elasticsearch search request body, e.g.
(s *Service) Search(ctx context.Context, index string, query any) (*SearchResult, error)
// UpdateItem merges doc's fields into the existing document at id - a partial update, not a full replace (see IndexItem for that).
(s *Service) UpdateItem(ctx context.Context, index, id string, doc any) (found bool, err error)
07

gist-fixtures

Spins up a real test dependency in Docker — MySQL, Postgres, MongoDB, Redis, RabbitMQ, and several others. It seeds that container with data straight from your own tagged Go structs, in one call. Handy for integration tests that need a real database instead of a mock.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
type string Container dialect to run (mysql / postgres / mariadb / mssql / mongodb / gcs / s3 / elasticsearch / rabbitmq / pubsub / mailpit / redis).
JSON
"gist-fixtures": [
  {
    "id": "customer-login",
    "enabled": true,
    "type": "mysql"
  }
]

type values

ValueDescription
mysqlMySQL container.
postgresPostgreSQL container.
mariadbMariaDB container.
mssqlMicrosoft SQL Server container (EULA auto-accepted).
mongodbMongoDB container.
gcsGoogle Cloud Storage emulator container; only image/host-port apply — buckets/objects come from fixture rows, not config.
s3Amazon S3 emulator container; only image/host-port apply — buckets/objects come from fixture rows, not config, seeded live after boot (no reliable init-mount path, unlike gcs).
elasticsearchElasticsearch container; only password applies (username is the fixed elastic superuser).
rabbitmqRabbitMQ container; username/password default to guest/guest and are also baked into the generated definitions.json.
pubsubGCP Pub/Sub emulator container; only project-id/host-port apply.
mailpitMailpit fake-SMTP/email-testing container; username/password are optional SMTP auth credentials (both required together to take effect) — no database concept.
redisRedis container; only password applies (sets --requirepass) — no username/database concept.

mysql / postgres / mariadb

Identical field behavior across all three — only the package/image differ.

FieldTypeDescription
databasestringCreates this database in the container. Must end in _test.
usernamestringCreates this user in the container.
passwordstringSets that user's password.
JSON
"gist-fixtures": [
  {
    "id": "orders-db-fixtures",
    "enabled": true,
    "type": "mysql",
    "database": "orderflow_test",
    "username": "orderflow",
    "password": "orderflow"
  }
]

mssql

SQL Server's admin account is always sa. EULA acceptance happens automatically.

FieldTypeDescription
databasestringSelects the target database. Must end in _test.
passwordstringSets the fixed sa account's password.
JSON
"gist-fixtures": [
  {
    "id": "orders-db-fixtures",
    "enabled": true,
    "type": "mssql",
    "database": "orderflow_test",
    "password": "OrderflowStrong!1"
  }
]

mongodb

FieldTypeDescription
databasestringSelects the target database. Must end in _test.
usernamestringCreates this user in the container.
passwordstringSets that user's password.
JSON
"gist-fixtures": [
  {
    "id": "orders-doc-fixtures",
    "enabled": true,
    "type": "mongodb",
    "database": "orderflow_test",
    "username": "orderflow",
    "password": "orderflow"
  }
]

gcs

Buckets and objects come entirely from GenerateFixtures' own rows (bucket/name/optional data per object) — Minimal/Full here differ only in the shared fields already covered above.

FieldTypeDescription
JSON
"gist-fixtures": [
  {
    "id": "receipts-fixtures",
    "enabled": true,
    "type": "gcs"
  }
]

s3

Buckets and objects come entirely from GenerateFixtures' own rows (bucket/name/optional data per object, the same shape as gcs) — Minimal/Full here differ only in the shared fields already covered above. Unlike gcs, seeding happens as a live call against the container's real S3 REST API once it's up, not a pre-boot file mount — S3Mock's own initial-bucket/object file layout isn't a stable convention across its supported image versions the way fake-gcs-server's is.

FieldTypeDescription
JSON
"gist-fixtures": [
  {
    "id": "receipts-fixtures",
    "enabled": true,
    "type": "s3"
  }
]

elasticsearch

The elastic superuser account name is fixed. The container's HTTP TLS layer is disabled, matching gist-elasticsearch-client; see Pointing a real client at your own fixtures container below.

FieldTypeDescription
passwordstringSets the fixed elastic superuser's password.
JSON
"gist-fixtures": [
  {
    "id": "order-index-fixtures",
    "enabled": true,
    "type": "elasticsearch",
    "password": "orderflow"
  }
]

rabbitmq

FieldTypeDescription
JSON
"gist-fixtures": [
  {
    "id": "bus-fixtures",
    "enabled": true,
    "type": "rabbitmq"
  }
]

pubsub

Names the project a real gist-pub-sub-client client points at to reach the same topics/subscriptions/messages rows.

FieldTypeDescription
project-idstringGCP project ID the emulator's topics/subscriptions are created under.
JSON
"gist-fixtures": [
  {
    "id": "order-events-fixtures",
    "enabled": true,
    "type": "pubsub",
    "project-id": "orderflow"
  }
]

mailpit

Leaving username and password unset, the container accepts unauthenticated SMTP.

FieldTypeDescription
JSON
"gist-fixtures": [
  {
    "id": "order-emails-fixtures",
    "enabled": true,
    "type": "mailpit"
  }
]

redis

Redis's classic AUTH is password-only. A logical DB index (0-15) is selected per-connection, not per-container.

FieldTypeDescription
passwordstringSets --requirepass directly on the container's own command line.
JSON
"gist-fixtures": [
  {
    "id": "order-cache-fixtures",
    "enabled": true,
    "type": "redis",
    "password": "orderflow"
  }
]

elasticsearch, pubsub, rabbitmq, mailpit, redis, and s3 containers get their seed data delivered by a live client call right after boot — same rows...any call, different table conventions per dialect. mailpit's is a single "messages" table: to (required — a row with none is skipped), from (defaults to fixtures@localhost), subject, body; each row is sent as one real SMTP delivery, visible through the running container's own web UI/REST API. redis's is a single "strings" table: key (required — a row with none is skipped), value (stringified, defaults to ""); each row is a real SET, sent over a direct RESP connection. s3's is the same single "objects" table gcs uses (bucket/name/optional data, a row missing either bucket or name is skipped) — each bucket is created once (an already-owned bucket is not an error) and each object is a real PutObject call.

Pointing a real client at your own fixtures container

Beyond seeding through GenerateFixtures, it's common to point the matching real client service — gist-google-cloud-storage-client, gist-aws-s3-client, gist-redis-client, gist-pub-sub-client, gist-elasticsearch-client, or gist-rabbit-mq-client — straight at a gist-fixtures-booted container for local dev. Pin the port with gist-fixtures' own host-port first, then:

ClientField(s)Notes
gist-google-cloud-storage-client endpoint: "http://127.0.0.1:<host-port>" endpoint skips ADC auth and talks straight to the fake server.
gist-aws-s3-client endpoint: "http://127.0.0.1:<host-port>", buckets mapping your own alias to the bucket name(s) you seeded endpoint switches the client to anonymous credentials and path-style addressing, matching S3Mock.
gist-redis-client host: "127.0.0.1", port: <host-port>, password matching the fixtures entry's own password Config.Password is wired into redis.Options.Password, authenticating against a --requirepass container.
gist-pub-sub-client endpoint: "127.0.0.1:<host-port>" (no scheme) Same host:port format as PUBSUB_EMULATOR_HOST, which this field takes precedence over.
gist-elasticsearch-client hosts: ["http://127.0.0.1:<host-port>"], user: "elastic", password matching the fixtures entry's own password (or "changeme" if left unset) gist-fixtures disables the container's HTTP TLS layer, matching the real client's plain http.Client.
gist-rabbit-mq-client url: "amqp://<user>:<password>@127.0.0.1:<host-port>/" — guest:guest unless the fixtures entry's own username/password were set A plain connection string, no TLS involved.

Go API

Go
package gistfixtures

GenerateFixtures(id string, rows ...any) gist.Option
08

gist-google-cloud-storage-client

Create, read, update, and delete buckets and objects in Google Cloud Storage, or a local emulator for testing. You refer to a bucket by a short name from your own config, not GCS's real bucket name. That mapping happens on the server side, so the real name never has to appear in your code.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
scope string OAuth scope used to build the Application Default Credentials token source.
JSON
"gist-google-cloud-storage-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "scope": "https://www.googleapis.com/auth/devstorage.read_write"
  }
]

Go API

Go
package gistgooglecloudstorageclient

NewService(server *gist.Server, serviceID string) *Service
// CreateBucket creates a new bucket - see gist-server's CreateBucket for the underlying REST call and its 409-is-not-an-error convention; created reports whether this call was the one that created it.
(s *Service) CreateBucket(ctx context.Context, bucketID, location string) (created bool, err error)
// DeleteBucket deletes the bucket - Cloud Storage requires it to be empty first (see gist-server's DeleteBucket).
(s *Service) DeleteBucket(ctx context.Context, bucketID string) (found bool, err error)
// DeleteObject deletes fileName from bucketID.
(s *Service) DeleteObject(ctx context.Context, bucketID, fileName string) (found bool, err error)
(s *Service) Exists(ctx context.Context, bucketID, fileName string) (bool, error)
// GetBucket retrieves the bucket's metadata, decoding it into out (a pointer, same convention json.Unmarshal uses).
(s *Service) GetBucket(ctx context.Context, bucketID string, out any) (found bool, err error)
// GetObject downloads fileName's raw content from bucketID.
(s *Service) GetObject(ctx context.Context, bucketID, fileName string) (found bool, content []byte, err error)
(s *Service) Store(ctx context.Context, bucketID, path string, fileHeader *multipart.FileHeader) (*string, *string, error)
// UpdateBucket merges attrs' fields into the bucket's metadata - e.g.
(s *Service) UpdateBucket(ctx context.Context, bucketID string, attrs any) (found bool, err error)
// UpdateObjectMetadata merges metadata into fileName's custom key/value metadata.
(s *Service) UpdateObjectMetadata(ctx context.Context, bucketID, fileName string, metadata map[string]string) (found bool, updated map[string]string, err error)
09

gist-http-client

A simple outbound HTTP client with one method per verb — GET, POST, PUT, PATCH, DELETE. Point it at a base URL once in config, then call it with just the path you want. Per-request details like headers or query params are passed in as small, typed options.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
base-url string 🔒 Base URL every request from this client is made against.
JSON
"gist-http-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "base-url": "https://api.paymentgateway.example.com"
  }
]

Go API

Go
package gisthttpclient

NewService(server *gist.Server, serviceID string) *Service
(s *Service) Delete(ctx context.Context, endpoint string, options ...delete.Option) Response
(s *Service) Get(ctx context.Context, endpoint string, options ...get.Option) Response
(s *Service) Patch(ctx context.Context, endpoint string, options ...patch.Option) Response
(s *Service) Post(ctx context.Context, endpoint string, options ...post.Option) Response
(s *Service) Put(ctx context.Context, endpoint string, options ...put.Option) Response
10

gist-http-server

Serves a directory of static files over HTTP. Point www-files at a folder — generated PDFs, images, whatever ends up there — and it's served as-is. No routing or code needed on your side.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
www-files string Directory path served as static files (http.FileServer).
JSON
"gist-http-server": [
  {
    "id": "customer-login",
    "enabled": true,
    "www-files": "./public/invoices"
  }
]
11

gist-mcp-bridge

Exposes other already-configured service instances' own operations as MCP (Model Context Protocol) tools, over its own standalone JSON-RPC-over-HTTP listener - point an AI agent (or any MCP client) at it instead of, or alongside, driving those instances directly.

bridged-services is a flat list of already-configured gist-api-server config ids to expose - one entry per instance you want reachable this way. Which of that instance's own endpoints actually show up is controlled per endpoint, by its own enable-mcp flag in endpoints.json - referencing an instance here exposes none of its endpoints until at least one is explicitly flagged.

Tool calls carry through whatever Authorization/x-api-key header the caller sent - the target endpoint's own real auth requirement (unchanged) is the only gate on what a tool call can actually do; this service performs no authorization of its own.

Setting access-key adds one coarse gate in front of that: every request must present it in an X-Mcp-Key header or get a plain 401, before tools/list or tools/call ever runs. It's not per-caller auth - anyone holding the key can call anything the bridge exposes - it exists to keep random/anonymous callers from even discovering what tools are available.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
bridged-services []string Config ids of already-configured gist-api-server instances this MCP server exposes tools from - each endpoint still needs its own enable-mcp flag to actually show up.
JSON
"gist-mcp-bridge": [
  {
    "id": "customer-login",
    "enabled": true,
    "bridged-services": ["orders-api"]
  }
]
12

gist-mysql-client

A MySQL connection pool with a small generic repo layer built in — Find, Save, Update, Delete — plus transactions and a simple condition/sort DSL. Call it straight off the injected service, e.g. sg.OrdersDB.Find[Order](...). gist-postgres-client (next) works exactly the same way, so switching databases barely changes your code. Set ssh to tunnel every connection (primary and replicas) through a bastion instead of dialing the database host directly.

Backups. Give the entry a backup object with enabled: true and it backs itself up on a schedule — off unless you do. Each backup is a mysqldump dump, compressed with zstd and encrypted with age to the public keys in recipients: the server can write backups but never read them, and the private key needed to restore stays offline. Everything is streamed, so memory use doesn't grow with the database.

Rotation. tiers keep the newest backup of each of the last keep periods — by default 7 daily, 5 weekly and 3 monthly, so deleted data is gone from every backup within about 3 months. One backup can count for several tiers and is stored once.

Where. Backups are made in local-path; with cloud (a gist-aws-s3-client or gist-google-cloud-storage-client entry of the same config) they are also copied there and rotated there too, and the local copy is deleted once the upload is complete unless cloud.keep-local. A backup is checked for room first (plus 512 MB always left free), made under a temporary name, verified before it's sealed, and only counts once its manifest exists — a crash or a full disk never leaves a broken backup, and leftovers are cleaned up before the next run.

Deleted personal data. List the tables to track and call RecordDeletion from the app just before deleting a row (e.g. a user). It's recorded in a tracking table, with the hash columns stored only as a keyed hash, and at once in a deletion journal next to the backups (and in the cloud). A restore then deletes those rows again even if they were deleted after the backup was taken — even when the database itself was lost — so restoring never brings deleted people back.

Commands. Run by the app binary (./app -backup=<entry id> <command>) or gist-server (-config config.json -backup <entry id> <command>): run (a backup now), list, find <table> <column>=<value> (which backups still hold a deleted row, and when the last expires), restore -identity <age key file> -into <new database> [-at <backup>] (always into a new database, which is dropped again if anything fails), record-deletion and drill-done (records a restore drill; one is asked for every drill-reminder). Needs mysqldump and mysql on the server; entries that connect through ssh can't be backed up.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
username string 🔒 Primary database username.
password string 🔒 Primary database password.
database string 🔒 Primary database name.
JSON
"gist-mysql-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "username": "orderflow",
    "password": "orderflow",
    "database": "orderflow"
  }
]

Go API

Go
package gistmysqlclient

// Count queues a Count[model] op; out is set once Run succeeds.
(b *Batch) Count[model any](out *int64, opts ...Option)
// Exists queues an Exists[model] op; out is set once Run succeeds.
(b *Batch) Exists[model any](out *bool, opts ...Option)
// Find queues a Find[model] op; out is set once Run succeeds.
(b *Batch) Find[model any](out *[]model, opts ...Option)
// FindOne queues a FindOne[model] op; out is set once Run succeeds.
(b *Batch) FindOne[model any](out **model, opts ...Option)
// Run sends every op queued on b so far as ONE BatchRepo RPC - Begin and the closing Rollback both folded into that same call, since a batch of reads has nothing to persist either way - then decodes each op's own response straight into the pointer its own queueing call was given.
(b *Batch) Run(ctx context.Context) error
AndConditions(cs ...Conditioner) Conditioner
NewBetweenCondition(field string, low, high any) Conditioner
NewCondition(field string, operator Operator, value any) Conditioner
NewNotBetweenCondition(field string, low, high any) Conditioner
OrConditions(cs ...Conditioner) Conditioner
WithConditions(conds ...Conditioner) Option
WithLimit(limit int64) Option
WithLock() Option
WithOffset(offset int64) Option
WithRange(start, end int64) Option
WithRelationConditions(relation string, conds ...Conditioner) Option
WithSorting(scends ...Scend) Option
NewService(server *gist.Server, serviceID string) *Service
(s *Service) Count[model any](opts ...Option) (int64, error)
(s *Service) Delete[model any](opts ...Option) (int64, error)
(s *Service) DeleteWithReturning[model any](opts ...Option) ([]model, int64, error)
(s *Service) Exists[model any](opts ...Option) (bool, error)
(s *Service) Find[model any](opts ...Option) ([]model, error)
(s *Service) FindOne[model any](opts ...Option) (*model, error)
(s *Service) InNamedReadTransaction(ctx context.Context, name string, fn func(tr *Transaction) error) error
(s *Service) InNamedTransaction(ctx context.Context, name string, fn func(tr *Transaction) error) error
(s *Service) InReadTransaction(ctx context.Context, fn func(tr *Transaction) error) error
// InTransaction, InReadTransaction, InNamedTransaction and InNamedReadTransaction are the callback form of NewTransaction/NewReadTransaction/NewNamedTransaction/ NewNamedReadTransaction: fn runs against a fresh Transaction that's committed if fn returns nil and rolled back otherwise, so a caller can't forget to close what it opens or leak it on an early return.
(s *Service) InTransaction(ctx context.Context, fn func(tr *Transaction) error) error
// NewBatch starts a new Batch against s.
(s *Service) NewBatch() *Batch
(s *Service) NewNamedReadTransaction(ctx context.Context, name string) (*Transaction, error)
(s *Service) NewNamedTransaction(ctx context.Context, name string) (*Transaction, error)
(s *Service) NewReadTransaction(ctx context.Context) (*Transaction, error)
(s *Service) NewTransaction(ctx context.Context) (*Transaction, error)
// RecordDeletion tells this database's backup (its config entry's "backup" object) that the app deleted a row of table - one of its "backup.track" tables, e.g.
(s *Service) RecordDeletion(ctx context.Context, table string, values map[string]string) (recorded bool, err error)
(s *Service) Save(models ...any) (int64, error)
(s *Service) SaveWithReturning[model any](models ...model) ([]model, int64, error)
(s *Service) Update(newValues any, opts ...Option) (int64, error)
(s *Service) UpdateWithReturning[model any](newValues any, opts ...Option) ([]model, int64, error)
// CloseAfter marks t to fold a Commit/Rollback into its very next Repo call - whichever op the caller calls next - instead of paying for a separate round trip afterward.
(t *Transaction) CloseAfter(action EndAction)
(t *Transaction) Commit() error
(tr *Transaction) Count[model any](opts ...Option) (int64, error)
(tr *Transaction) Delete[model any](opts ...Option) (int64, error)
(tr *Transaction) DeleteWithReturning[model any](opts ...Option) ([]model, int64, error)
(tr *Transaction) Exists[model any](opts ...Option) (bool, error)
(tr *Transaction) Find[model any](opts ...Option) ([]model, error)
(tr *Transaction) FindOne[model any](opts ...Option) (*model, error)
(t *Transaction) Rollback() error
(tr *Transaction) Save(models ...any) (int64, error)
(tr *Transaction) SaveWithReturning[model any](models ...model) ([]model, int64, error)
(tr *Transaction) Update(newValues any, opts ...Option) (int64, error)
(tr *Transaction) UpdateWithReturning[model any](newValues any, opts ...Option) ([]model, int64, error)
13

gist-parameter-store

Centralizes any named value - a host IP, a username, a database password, an API key - behind one alias per source: a plain literal stored directly in config.json, an environment variable (config.json names the variable, not the value), or Google Cloud Secret Manager (or a local emulator for testing) for values that actually need secret-manager-grade handling. Every other field elsewhere in config.json can reference an alias here - gist-ps://<instance-id>/<source>/<alias> - instead of holding the real value directly; a customer's own app code can also fetch one live via AccessValue. The real GCP secret id never has to appear in your code.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
JSON
"gist-parameter-store": [
  {
    "id": "customer-login",
    "enabled": true
  }
]

Go API

Go
package gistparameterstore

NewService(server *gist.Server, instanceID string) *Service
// AccessValue fetches alias's own value from source ("plain", "env", or "gcp-sm" - matching this instance's own config.json plain/env/gcp-sm maps) at version (gcp-sm only; empty means "latest", ignored otherwise).
(s *Service) AccessValue(ctx context.Context, source, alias, version string) (payload []byte, found bool, err error)
// AddGCPSecretVersion adds payload as a new version of alias, in this instance's own gcp-sm backend.
(s *Service) AddGCPSecretVersion(ctx context.Context, alias string, payload []byte) (versionName string, err error)
// CreateGCPSecret creates a new, empty secret container for alias in this instance's own gcp-sm backend - see AddGCPSecretVersion for giving it a value; created is false (not an error) if it already existed.
(s *Service) CreateGCPSecret(ctx context.Context, alias string) (created bool, err error)
// DeleteGCPSecret deletes alias and every one of its versions, from this instance's own gcp-sm backend.
(s *Service) DeleteGCPSecret(ctx context.Context, alias string) (found bool, err error)
14

gist-postgres-client

A PostgreSQL connection pool with the same generic repo layer as gist-mysql-client — Find, Save, Update, Delete — plus transactions and a simple condition/sort DSL. Call it straight off the injected service, e.g. sg.OrdersDB.Find[Order](...). gist-mysql-client (previous) is API-identical, so switching between the two databases barely changes your code. Set ssh to tunnel every connection (primary and replicas) through a bastion instead of dialing the database host directly.

Backups. Give the entry a backup object with enabled: true and it backs itself up on a schedule — off unless you do. Each backup is a pg_dump dump, compressed with zstd and encrypted with age to the public keys in recipients: the server can write backups but never read them, and the private key needed to restore stays offline. Everything is streamed, so memory use doesn't grow with the database.

Rotation. tiers keep the newest backup of each of the last keep periods — by default 7 daily, 5 weekly and 3 monthly, so deleted data is gone from every backup within about 3 months. One backup can count for several tiers and is stored once.

Where. Backups are made in local-path; with cloud (a gist-aws-s3-client or gist-google-cloud-storage-client entry of the same config) they are also copied there and rotated there too, and the local copy is deleted once the upload is complete unless cloud.keep-local. A backup is checked for room first (plus 512 MB always left free), made under a temporary name, verified before it's sealed, and only counts once its manifest exists — a crash or a full disk never leaves a broken backup, and leftovers are cleaned up before the next run.

Deleted personal data. List the tables to track and call RecordDeletion from the app just before deleting a row (e.g. a user). It's recorded in a tracking table, with the hash columns stored only as a keyed hash, and at once in a deletion journal next to the backups (and in the cloud). A restore then deletes those rows again even if they were deleted after the backup was taken — even when the database itself was lost — so restoring never brings deleted people back.

Commands. Run by the app binary (./app -backup=<entry id> <command>) or gist-server (-config config.json -backup <entry id> <command>): run (a backup now), list, find <table> <column>=<value> (which backups still hold a deleted row, and when the last expires), restore -identity <age key file> -into <new database> [-at <backup>] (always into a new database, which is dropped again if anything fails), record-deletion and drill-done (records a restore drill; one is asked for every drill-reminder). Needs pg_dump and pg_restore at least as new as the database server on the server; entries that connect through ssh can't be backed up.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
username string 🔒 Primary database username.
password string 🔒 Primary database password.
database string 🔒 Primary database name.
migration MigrationConfig gist-server -migrate against this config entry - never runs on its own, only when explicitly invoked (-migrate <this entry's own id>), and only once folder-path/source are both set - leave the whole object out entirely for a config entry that never needs migrations.
JSON
"gist-postgres-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "username": "orderflow",
    "password": "orderflow",
    "database": "orderflow",
    "migration": {
      "folder-path": "./migrations",
      "source": "public.gist_migrations"
    }
  }
]

Go API

Go
package gistpostgresclient

AndConditions(cs ...Conditioner) Conditioner
NewBetweenCondition(field string, low, high any) Conditioner
NewCondition(field string, operator Operator, value any) Conditioner
NewNotBetweenCondition(field string, low, high any) Conditioner
OrConditions(cs ...Conditioner) Conditioner
WithConditions(conds ...Conditioner) Option
WithLimit(limit int64) Option
WithLock() Option
WithOffset(offset int64) Option
WithRange(start, end int64) Option
WithRelationConditions(relation string, conds ...Conditioner) Option
WithSorting(scends ...Scend) Option
NewService(server *gist.Server, serviceID string) *Service
(s *Service) Count[model any](opts ...Option) (int64, error)
(s *Service) Delete[model any](opts ...Option) (int64, error)
(s *Service) DeleteWithReturning[model any](opts ...Option) ([]model, int64, error)
(s *Service) Exists[model any](opts ...Option) (bool, error)
(s *Service) Find[model any](opts ...Option) ([]model, error)
(s *Service) FindOne[model any](opts ...Option) (*model, error)
(s *Service) InNamedReadTransaction(ctx context.Context, name string, fn func(tr *Transaction) error) error
(s *Service) InNamedTransaction(ctx context.Context, name string, fn func(tr *Transaction) error) error
(s *Service) InReadTransaction(ctx context.Context, fn func(tr *Transaction) error) error
// InTransaction, InReadTransaction, InNamedTransaction and InNamedReadTransaction are the callback form of NewTransaction/NewReadTransaction/NewNamedTransaction/ NewNamedReadTransaction: fn runs against a fresh Transaction that's committed if fn returns nil and rolled back otherwise, so a caller can't forget to close what it opens or leak it on an early return.
(s *Service) InTransaction(ctx context.Context, fn func(tr *Transaction) error) error
(s *Service) NewNamedReadTransaction(ctx context.Context, name string) (*Transaction, error)
(s *Service) NewNamedTransaction(ctx context.Context, name string) (*Transaction, error)
(s *Service) NewReadTransaction(ctx context.Context) (*Transaction, error)
(s *Service) NewTransaction(ctx context.Context) (*Transaction, error)
// RecordDeletion tells this database's backup (its config entry's "backup" object) that the app deleted a row of table - one of its "backup.track" tables, e.g.
(s *Service) RecordDeletion(ctx context.Context, table string, values map[string]string) (recorded bool, err error)
(s *Service) Save[model any](models ...model) (int64, error)
(s *Service) SaveWithReturning[model any](models ...model) ([]model, int64, error)
(s *Service) Update(newValues any, opts ...Option) (int64, error)
(s *Service) UpdateWithReturning[model any](newValues any, opts ...Option) ([]model, int64, error)
// CloseAfter marks t to fold a Commit/Rollback into its very next Repo call - whichever op the caller calls next - instead of paying for a separate round trip afterward.
(t *Transaction) CloseAfter(action EndAction)
(t *Transaction) Commit() error
(tr *Transaction) Count[model any](opts ...Option) (int64, error)
(tr *Transaction) Delete[model any](opts ...Option) (int64, error)
(tr *Transaction) DeleteWithReturning[model any](opts ...Option) ([]model, int64, error)
(tr *Transaction) Exists[model any](opts ...Option) (bool, error)
(tr *Transaction) Find[model any](opts ...Option) ([]model, error)
(tr *Transaction) FindOne[model any](opts ...Option) (*model, error)
(t *Transaction) Rollback() error
(tr *Transaction) Save[model any](models ...model) (int64, error)
(tr *Transaction) SaveWithReturning[model any](models ...model) ([]model, int64, error)
(tr *Transaction) Update(newValues any, opts ...Option) (int64, error)
(tr *Transaction) UpdateWithReturning[model any](newValues any, opts ...Option) ([]model, int64, error)
15

gist-pub-sub-client

A Publish/Pull/Ack API for a Pub/Sub-style message queue, built on plain REST calls. Pull is a synchronous poll — you ask for messages on your own schedule and acknowledge whatever you've actually processed. If you'd rather have messages pushed to you instead, see gist-rabbit-mq-client below.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
project-id string GCP project ID; used to build the topics-list readiness check URL.
JSON
"gist-pub-sub-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "project-id": "orderflow"
  }
]

Go API

Go
package gistpubsubclient

NewService(server *gist.Server, serviceID string) *Service
// Ack acknowledges every ID in ackIDs against subscription, so Pub/Sub doesn't redeliver them.
(s *Service) Ack(ctx context.Context, subscription string, ackIDs ...string) error
// Publish sends one message to topic (short name, e.g.
(s *Service) Publish(ctx context.Context, topic string, data []byte, attributes map[string]string) (messageID string, err error)
// Pull polls subscription once for up to maxMessages waiting messages.
(s *Service) Pull(ctx context.Context, subscription string, maxMessages int32) ([]PulledMessage, error)
16

gist-push-client

Send push notifications to iPhones and iPads through Apple's APNs and to Android devices through Google's FCM. The credentials stay in the server's config; your code sends a message to a list of device tokens and gets one result per device back - including which tokens no longer exist, so you can delete them.

Text can be sent as localization keys the phone's OS looks up in the app's own strings (so pushes arrive in the phone's language without the server knowing any translations), or as plain text. A silent push carries only data and wakes the app without showing anything.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
JSON
"gist-push-client": [
  {
    "id": "customer-login",
    "enabled": true
  }
]

Go API

Go
package gistpushclient

NewService(server *gist.Server, serviceID string) *Service
// Send pushes m to every target and returns one result per target, in order.
(s *Service) Send(ctx context.Context, targets []Target, m Message) ([]Result, error)
17

gist-rabbit-mq-client

A real AMQP 0-9-1 client connection to RabbitMQ. Publish messages and register consumers directly, with messages pushed to you as they arrive. It's the push-based counterpart to gist-pub-sub-client's polling model.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
url string 🔒 Full AMQP connection URL — credentials and vhost live in the URL.
JSON
"gist-rabbit-mq-client": [
  {
    "id": "customer-login",
    "enabled": true,
    "url": "amqp://orderflow:orderflow@127.0.0.1:5672/"
  }
]

Go API

Go
package gistrabbitmqclient

NewService(server *gist.Server, serviceID string) *Service
// ExchangeDeclare declares exchange name of the given kind ("direct" | "fanout" | "topic" | "headers").
(s *Service) ExchangeDeclare(ctx context.Context, name, kind string, durable, autoDelete bool, args map[string]string) error
// Publish sends one message.
(s *Service) Publish(ctx context.Context, exchange, routingKey string, body []byte, contentType string, headers map[string]string) error
// QueueBind binds queue to exchange under routingKey.
(s *Service) QueueBind(ctx context.Context, queue, exchange, routingKey string, args map[string]string) error
// QueueDeclare declares a queue and returns its real name - only differs from name when name was empty, letting the broker generate one (RabbitMQ's usual pattern for a private, per-consumer queue).
(s *Service) QueueDeclare(ctx context.Context, name string, durable, autoDelete, exclusive bool, args map[string]string) (string, error)
18

gist-redis-client

A simple key/value cache backed by Redis — Get, Set, Del, Exists, Incr, IncrBy, Expire. It's exposed through one injectable service, so you call it straight off your own services group. See the Go API reference below for the exact method signatures.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
JSON
"gist-redis-client": [
  {
    "id": "customer-login",
    "enabled": true
  }
]

Go API

Go
package gistredisclient

NewService(server *gist.Server, serviceID string) *Service
// Del removes keys, returning how many actually existed to be removed.
(s *Service) Del(ctx context.Context, keys ...string) (deleted int64, err error)
// Exists reports how many of keys exist - a key repeated in the list counts twice, matching Redis's own EXISTS semantics.
(s *Service) Exists(ctx context.Context, keys ...string) (count int64, err error)
// Expire sets key's TTL.
(s *Service) Expire(ctx context.Context, key string, ttl time.Duration) (existed bool, err error)
// Get returns key's value.
(s *Service) Get(ctx context.Context, key string) (value []byte, found bool, err error)
// Incr increments key by 1 (creating it at 0 first if it doesn't exist) and returns the new value.
(s *Service) Incr(ctx context.Context, key string) (value int64, err error)
// IncrBy increments key by by and returns the new value.
(s *Service) IncrBy(ctx context.Context, key string, by int64) (value int64, err error)
// Set stores value under key.
(s *Service) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
19

gist-scheduler

Runs a cron job inside gist-server and calls back into your own process on every tick. You register what should happen on that tick as a plain function — gist-server only handles the timing. There's no queue or worker process to manage, just a scheduled callback.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
schedule string Standard cron expression controlling when the tick callback fires.
JSON
"gist-scheduler": [
  {
    "id": "customer-login",
    "enabled": true,
    "schedule": "0 3 * * *"
  }
]

Go API

Go
package gist

gist.RegisterScheduleFunc[dep any](schedulerID string, d dep, fn func(dep)) Option
20

gist-state-machine

A simple state machine: given an object's current state and a trigger name, it checks a configured transition graph and decides whether the move is allowed. Your own code still does the actual work — saving the object happens in the trigger's own callback, not here. You can also render the whole graph as a diagram (DOT, or a self-contained SVG) to see the flow at a glance.

Field Type Description
id string Unique instance ID.
enabled bool If false, this entry is skipped entirely.
initial-state string Default starting state for a new object under this machine.
transitions-file string Path to the transitions JSON file, loaded into Transitions.
JSON
"gist-state-machine": [
  {
    "id": "customer-login",
    "enabled": true,
    "initial-state": "pending",
    "transitions-file": "./config/order-transitions.json"
  }
]

Go API

Go
package giststatemachine

// Attach registers every one of handlers against serviceID - the id is named once here, not once per trigger.
Attach[servicesGroup any, M Statabler](serviceID string, handlers ...*TriggerHandler[servicesGroup, M]) gist.Option
// Transition fires trigger on statable against svc: every OnEnter function in order, the transition attempt itself, OnAction, then every OnExit function in order - see the package doc for the full sequencing and abort semantics.
Transition[M Statabler](ctx context.Context, svc *Service, trigger string, statable M) error
NewService(server *gist.Server, serviceID string) *Service
// Graph returns the DOT-format representation of svc's configured transition graph (https://graphviz.org/doc/info/lang.html) - render it with any Graphviz front-end (e.g.
(svc *Service) Graph(ctx context.Context) (string, error)
// GraphSVG returns a self-contained, hand-rendered SVG diagram of the same graph Graph describes - no external Graphviz/layout engine needed to view it, just an SVG-capable viewer (any browser, or an <img>/<object> tag in a page your own process already serves).
(svc *Service) GraphSVG(ctx context.Context) (string, error)
// MultiLaneGraphSVG returns one combined, hand-rendered SVG spanning every one of serviceIDs' configured gist-state-machine instances, each its own horizontal swimlane, connected wherever a transition's to-service names another instance in the list - see gist-server's own RenderMultiLaneSVG.
(svc *Service) MultiLaneGraphSVG(ctx context.Context, serviceIDs []string) (string, error)
(s *Statable) GetState() string
(s *Statable) SetState(state string)
// RegisterTriggerFunc builds one trigger: its name, its required OnAction (the trigger's own business logic - the reason it exists), and any OnEnter/OnExit phases via the OnEnter/OnExit options.
RegisterTriggerFunc[servicesGroup any, M Statabler](trigger string, onAction TransitionFn[servicesGroup, M], opts ...TriggerOption[servicesGroup, M]) *TriggerHandler[servicesGroup, M]
// OnEnter adds fns to a trigger's guard/preparation phase, run in the order given (appended to any already added by an earlier OnEnter option), before the transition is attempted - an error here aborts it, statable's state is left untouched.
OnEnter[servicesGroup any, M Statabler](fns ...TransitionFn[servicesGroup, M]) TriggerOption[servicesGroup, M]
// OnExit adds fns to a trigger's side-effect phase, run in the order given (appended to any already added by an earlier OnExit option), after OnAction succeeds.
OnExit[servicesGroup any, M Statabler](fns ...TransitionFn[servicesGroup, M]) TriggerOption[servicesGroup, M]