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

project

package
v0.6.7 Latest Latest
Warning

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

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

Documentation

Overview

Package project reads rig.yaml, the file that marks the root of a rig project and configures everything that is not per-table.

Discovery walks up from the working directory, so rig commands work from anywhere inside a project. Every path in the file resolves relative to the file's own directory rather than the working directory, so the same configuration means the same thing regardless of where it is run from.

Index

Constants

View Source
const (
	// DefaultCacheTTL is thirty seconds.
	//
	// Long enough that a person clicking through an application is served from
	// memory throughout, and short enough that the case the backstop is for — a
	// replica that lost the channel between the revocation and the reconnect —
	// is measured in seconds. It is not the number that makes revocation
	// immediate; the notification is.
	DefaultCacheTTL = 30 * time.Second

	// DefaultCacheChannel matches cache.DefaultChannel, spelled here because
	// this package configures the runtime rather than importing it.
	DefaultCacheChannel = "rig_cache"

	// DefaultCacheMaxEntries matches cache.DefaultMaxEntries, for the same
	// reason.
	DefaultCacheMaxEntries = 50_000
)
View Source
const (
	DefaultTableDir   = "services/{table}"
	DefaultConfigFile = "{table_dir}/{table}.yaml"

	DefaultAPIVersion   = "v1"
	DefaultSearchMethod = SearchBoth

	// DefaultRevisionHeader carries the API revision, in both directions.
	//
	// This is the one place the name is decided. Both ends of the conversation
	// are generated from the document that records it, so the server reading it
	// and the client sending it cannot disagree — rigclient carries the same
	// literal only as a fallback for a client somebody built by hand.
	DefaultRevisionHeader = "API-Revision"

	DefaultImage    = "postgres:17-alpine"
	DefaultPort     = 55432
	DefaultDBName   = "rig"
	DefaultDBUser   = "rig"
	DefaultDBPass   = "rig"
	DefaultDBSchema = "public"

	// DefaultElectricImage pins the sync service, because "latest" is a
	// different container on every machine and live sync is exactly the place a
	// version skew looks like an application bug.
	DefaultElectricImage = "electricsql/electric:1.6.9"
	// DefaultElectricPort sits beside DefaultPort the way the containers sit
	// beside each other. internal/dockerdb/ports.go lists the same number, and
	// its uniqueness test is what keeps a suite from ever taking it.
	DefaultElectricPort = 55433

	DefaultMigrationsDir = "migrations"

	// DefaultMigrationsTable is rig/migrate's, not a copy of it: `rig db up`
	// and a binary migrating itself have to read the same bookkeeping, and two
	// constants that happen to match today would not.
	DefaultMigrationsTable = migrate.DefaultTable

	// DefaultFoundation keeps rig's own migrations in the project's own
	// directory. It is the conservative half of the choice: the directory stays
	// the whole truth about the database, which is worth more than saving a
	// thousand lines of SQL until somebody asks to trade it.
	DefaultFoundation = FoundationVendored

	DefaultJSONCase = "camel"

	// DefaultTenantQuery is the parameter the query tenant source reads.
	DefaultTenantQuery = "tenant"
	// DefaultSigningKeyEnv is where the key that signs the OAuth state parameter
	// is read from. It has no alternative source: it is a secret, so a variable
	// is the only place it can come from and defaulting the name costs nothing.
	DefaultSigningKeyEnv = "OAUTH_SIGNING_KEY"

	// DefaultCallbackPath is the route on the front end that receives a
	// finished provider sign-in.
	DefaultCallbackPath = "/auth/callback"
	// DefaultCORSOriginsEnv is where a deployment overrides the allowed origin
	// list. Defaulted, unlike the signing key's variable, because the value is
	// not a secret and a project that never sets it loses nothing.
	DefaultCORSOriginsEnv = "CORS_ORIGINS"
	// DefaultCORSMaxAge is how long a browser may cache a preflight. Ten
	// minutes because that is the longest Safari honours, so a larger number
	// would be a value rig let somebody write and no browser read.
	DefaultCORSMaxAge = 10 * time.Minute

	// DefaultFilesBackend is memory, which is not durable. A project that means
	// to keep its uploads has to say so, and a default of s3 would need a bucket
	// nobody configured.
	DefaultFilesBackend = BackendMemory

	// DefaultFilesMaxBytes is 25 MiB: comfortably more than a photograph from a
	// phone, comfortably less than something that should have been a multipart
	// upload to a bucket.
	DefaultFilesMaxBytes = 25 << 20

	// DefaultFilesAbandonedAfter gives a pending upload a day before the sweeper
	// treats it as the remains of a request that died. Generous on purpose: the
	// cost of waiting is a row, and the cost of being wrong is deleting a file
	// somebody is still sending.
	DefaultFilesAbandonedAfter = 24 * time.Hour

	// DefaultFilesRestoreWindow matches the thirty days `restore_window_days`
	// scaffolds for an ordinary table, so a file and the row pointing at it go
	// out of reach together.
	DefaultFilesRestoreWindow = 30 * 24 * time.Hour

	// DefaultNotificationDigest is Immediate: an account that has said nothing
	// hears about things when they happen. Anything else would be rig deciding
	// on somebody's behalf that they wanted less mail than the application
	// thought it was sending.
	DefaultNotificationDigest = DigestImmediate

	// DefaultClaimTTL is five minutes, which is the wrong number for somebody
	// and has to be some number.
	//
	// It is chosen for the slowest channel most applications have, which is
	// mail: an SMTP conversation or a provider API call that has not finished in
	// five minutes has failed. A project whose only channel is a websocket push
	// that either works in 200ms or does not should set this far lower, and one
	// with a provider that retries internally for ten minutes has to set it
	// higher — the relationship to the channel's own timeout is the one
	// misconfiguration here worth understanding.
	DefaultClaimTTL = 5 * time.Minute

	// DefaultNotificationSendTimeout bounds one call into a channel.
	//
	// Thirty seconds, which is what rigclient allows a whole request. It is the
	// number that makes DefaultClaimTTL's paragraph above checkable rather than
	// advisory: the relationship it calls "the one misconfiguration here worth
	// understanding" is between these two values, and with both of them in this
	// file rig can refuse the pair instead of describing it.
	//
	// A channel is the one outbound call rig does not make itself, so it is the
	// one that could not bound itself. Everything else already does — three
	// seconds for the breach check, ten for a token exchange.
	DefaultNotificationSendTimeout = 30 * time.Second

	// DefaultMaxAttempts is fourteen, after which a delivery is Failed and stops
	// being claimed. Without a cap a permanently broken address consumes a lease
	// and a log line forever.
	//
	// It was five, and five at a doubling minute spans thirty-one minutes. That
	// outlasts a blip and nothing else: a provider down for a morning had every
	// message rig held for it permanently failed, and the row said Failed rather
	// than "was never really tried". The number that matters is the total, and an
	// outage is measured in hours by everyone who has had one.
	DefaultMaxAttempts = 14

	// DefaultBackoffBase is a minute, doubling: one, two, four, eight, sixteen,
	// thirty-two, and then the cap.
	DefaultBackoffBase = time.Minute

	// DefaultBackoffCap is an hour, and it is what makes fourteen attempts a
	// schedule rather than a sleep.
	//
	// Doubling from a minute fourteen times is five days. Capped here, the
	// fourteen attempts span about eight hours: 1m, 2m, 4m, 8m, 16m, 32m, and
	// then seven waits at the hour. The trade against the old thirty-one minutes
	// is that a genuinely undeliverable message now occupies a row for eight
	// hours — notify.Permanent is the answer to that, and it is why these numbers
	// could be raised at all.
	DefaultBackoffCap = time.Hour

	// DefaultNotificationRetention is ninety days. Long enough that "what was I
	// told in the spring" has an answer, and short enough that the busiest table
	// in the schema does not grow forever — which every other table in rig
	// currently does, and which is named as known and unfixed.
	DefaultNotificationRetention = 90 * 24 * time.Hour

	// The presence block's numbers. Each is the cost of the feature stated one
	// way, and the presence module carries the same four — being resolved here is
	// what lets the generated wiring pass them explicitly rather than relying on
	// the module's own zero handling, so `rig schema project` can quote what a
	// deployment is actually running.
	//
	// DefaultPresenceTTL is a minute: a generous window for a ghost, affordable
	// because an ordinary close sends a leave, so the TTL only covers a crashed
	// tab and a dead network.
	DefaultPresenceTTL = time.Minute
	// DefaultPresenceHeartbeat is a third of the TTL, which is the floor
	// checkPresence enforces — three beats before somebody vanishes.
	DefaultPresenceHeartbeat = 20 * time.Second
	// DefaultPresenceSweep is how often the in-process sweeper looks. It is
	// housekeeping rather than a guarantee: nothing about who is present waits
	// for it.
	DefaultPresenceSweep = time.Minute
	// DefaultPresenceGrace keeps an expired row for five minutes after the last
	// subscriber stopped drawing it, so the reader's arithmetic and the
	// sweeper's can never disagree in the direction that makes a row come back.
	DefaultPresenceGrace = 5 * time.Minute

	// The API throttle defaults. Deliberately loose: these are a backstop
	// against a client in a retry loop and against one tenant crowding out the
	// rest, not a quota. A default tight enough to be interesting is one that
	// breaks somebody's working integration the day they upgrade.
	//
	// The ladder is machines, then people, then strangers. An integration is
	// allowed the most because that is what an integration is for; one person
	// clicking cannot reach a thousand a minute without something being wrong;
	// and an address nobody has authenticated is the loosest identity there is,
	// so it gets the least.
	DefaultThrottleAPIKeyMax  = 5000
	DefaultThrottleAccountMax = 1000
	DefaultThrottleTenantMax  = 10000
	DefaultThrottleIPMax      = 300
	// DefaultThrottleWindow is the window all four share. One window keeps the
	// four numbers comparable, which is what makes the ladder above readable as
	// a ladder.
	DefaultThrottleWindow = time.Minute

	// The AWS SDK's own variable names, so a deployment that already has
	// credentials in the environment needs no configuration at all.
	DefaultAccessKeyEnv = "AWS_ACCESS_KEY_ID"
	DefaultSecretKeyEnv = "AWS_SECRET_ACCESS_KEY"
)

Defaults every project inherits. They are the shape `rig init` writes, so a generated rig.yaml can stay short: anything left out here means the same thing as spelling it out.

View Source
const (
	// BackendMemory keeps objects in a map. For tests and `go run`, and not
	// durable — every upload is lost on a restart.
	BackendMemory = "memory"
	// BackendS3 is S3 and anything speaking its API.
	BackendS3 = "s3"
)

The backends a project can keep its bytes in.

View Source
const (
	// ImageEnv overrides `database.image`.
	ImageEnv = "RIG_DB_IMAGE"
	// ElectricImageEnv overrides `database.electric.image`.
	ElectricImageEnv = "RIG_ELECTRIC_IMAGE"
)

The environment variables that name the containers `rig db up` starts.

`database.image` and `database.electric.image` in rig.yaml are the project's answer, and they are the right place for it: one image, written down, the same on every machine. These exist for the case that file cannot express — an image whose *address* differs by who is pulling it.

The case that forced them is a private registry. An image mirrored into a company's own ECR is reached at <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>, and the account is not a fact about the project: it is a fact about the deployment doing the pulling. Committing one into rig.yaml puts a second organisation's account in a file every laptop reads, and leaves a fork of the project pointing at a registry it cannot reach. It also stops being hypothetical the day an upstream image goes behind a login, which is what `electricsql/electric` did — every tag of it, not one.

So the variable wins over the file. That is the opposite of how rig treats everything else, and the reason is that this is the one setting whose correct value is a property of the environment rather than of the project: a continuous integration job that has just logged in to a registry knows something rig.yaml cannot be told once and for all.

View Source
const (
	// DefaultMonitorBasePath is where the page is mounted on its own listener.
	// Nothing else is on that listener to collide with; the prefix is kept
	// because it is the URL projects already have, and because it gives a
	// reverse proxy in front of the port something to key on.
	DefaultMonitorBasePath = "/_rig/monitor"

	// DefaultMonitorMaxTraces is how many requests the page lists.
	DefaultMonitorMaxTraces = 200

	// DefaultMonitorMaxLogs is how many log lines the page reads. Larger than
	// the traces, because one request writes several lines and the request line
	// alone means at least one per request listed.
	DefaultMonitorMaxLogs = 500

	// DefaultMonitorPasswordEnv is the variable the page reads its password
	// from when the project names no other.
	DefaultMonitorPasswordEnv = "RIG_MONITOR_PASSWORD"
)

The monitoring page's defaults.

Spelled here rather than taken from rig/observe, which is where the page lives and declares the same three. `auth:` does the opposite — DefaultAuthBasePath is auth.DefaultBasePath — and the difference is what the import would cost: rig depends on rig/auth already, and depending on rig/observe would put OpenTelemetry in the CLI's own binary, for three strings. They have to agree, and nothing checks that they do; a rig.yaml always resolves these before a generator sees them, so observe's copies are reached only by a hand-written main that left the fields zero.

View Source
const (
	// DigestImmediate sends each delivery on its own, as soon as it is due.
	DigestImmediate = "Immediate"
	DigestHourly    = "Hourly"
	DigestDaily     = "Daily"
	DigestWeekly    = "Weekly"
	// DigestOff sends nothing on that channel and still writes the inbox line.
	// It is not the same as turning the channel off, and both exist: `Off` is
	// about this account's preference, `is_enabled: false` is about the channel.
	DigestOff = "Off"
)

The digest windows an account can ask for, and the one that is not a window.

View Source
const DefaultThrottleInterval = time.Second

DefaultThrottleInterval is how long a replica holds its count by default.

A second is chosen from what it costs rather than from what it buys: it turns a write per request into a write per second per caller, and the traffic it can miss in that second is small next to limits measured in thousands per minute.

View Source
const DurationPattern = `^(0|(\d+(\.\d+)?(ns|us|µs|ms|s|m|h|d))+)$`

DurationPattern is the JSON Schema pattern a duration must match. It is what an editor validates against, and it is deliberately the same expression the emitted schema publishes.

View Source
const LongestDigestWindow = 7 * 24 * time.Hour

LongestDigestWindow is the widest window a digest can wait for, and therefore the floor under Notifications.Retention.

View Source
const MinClaimTTL = time.Minute

MinClaimTTL is the shortest lease rig will start with.

A lease shorter than this is a lease that expires while an ordinary HTTP call to a mail provider is still in flight, which makes every message claimed twice under normal load rather than only after a crash. Refused at boot rather than discovered as duplicate mail.

View Source
const MinPresenceHeartbeat = time.Second

MinPresenceHeartbeat is the shortest heartbeat the document can carry, which is the only reason there is one: every duration in this block is resolved to whole seconds for the IR, so anything under a second arrives as zero and zero reads as unset.

View Source
const MinPresenceTTL = 15 * time.Second

MinPresenceTTL is the shortest window rig will start with.

Under fifteen seconds presence flickers on an ordinary mobile connection, and what that presents as is a broken feature rather than a number somebody chose. The presence module refuses the same value at construction; this is the earlier and cheaper place to notice.

View Source
const MinSendTimeout = time.Second

MinSendTimeout is the shortest send timeout the document can carry, which is the only reason there is one: every duration in this block is resolved to whole seconds for the IR, so anything under a second arrives as zero and zero reads as unset.

View Source
const PresenceBeatsBeforeGone = 3

PresenceBeatsBeforeGone is how many heartbeats have to be lost before a session disappears, and therefore the ratio the TTL is checked against.

Three, and each one is a different failure: one for a garbage-collection pause, one for a slow network, one for the request that was actually lost. At two, an ordinary hiccup makes somebody vanish from everybody else's screen.

View Source
const SchemaID = "https://rig.dev/schema/project.v1.json"

SchemaID is the identifier the emitted project schema publishes itself under.

Variables

View Source
var (
	DefaultAuthBasePath = auth.DefaultBasePath
	DefaultTenantHeader = auth.TenantHeader

	DefaultAccessTTL      = session.DefaultAccessTTL
	DefaultRefreshTTL     = session.DefaultRefreshTTL
	DefaultRememberTTL    = session.DefaultRememberTTL
	DefaultRotationLeeway = session.DefaultRotationLeeway
	DefaultIdentityTTL    = session.DefaultIdentityTTL
	DefaultStateTTL       = oauth.DefaultStateTTL

	DefaultEmailCodeLength      = account.DefaultEmailCodeLength
	DefaultEmailCodeTTL         = account.DefaultEmailCodeTTL
	DefaultEmailCodeMaxAttempts = account.DefaultEmailCodeMaxAttempts
)

The authentication defaults are rig/auth's own rather than copies of them. Two constants that happen to match today would not stay matching, and a generated wiring that passed 10m while the module's default was something else would be documenting a lifetime the server does not have.

View Source
var ConfigNames = []string{"rig.yaml", "rig.yml"}

ConfigNames are the filenames rig looks for, in order.

View Source
var ErrNotFound = errors.New("no rig.yaml found in this directory or any parent")

ErrNotFound is returned by Find when no configuration file exists.

Functions

func Backends

func Backends() []string

Backends is every accepted value, for the schema and for the diagnostic.

func DefaultInlineTypes

func DefaultInlineTypes() []string

DefaultInlineTypes are the sniffed types served without an attachment disposition.

Images and nothing else. A file served inline from the API origin runs on that origin, and the URL is in a synced row that ends up in an <img> without anybody thinking about it — so text/html, image/svg+xml and application/pdf are all deliberately absent. SVG is the one that surprises people: it is an image, and it can carry script.

func Digests

func Digests() []string

Digests is every accepted value, for the schema and for the diagnostic.

func Find

func Find(start string) (string, error)

Find locates the configuration file by walking up from start.

The walk stops at the first match, at a directory containing .git, or at the filesystem root. Stopping at a repository boundary keeps a stray rig.yaml in a home directory from silently capturing an unrelated project.

func Schema

func Schema() ([]byte, error)

Schema returns the JSON Schema for rig.yaml.

Types

type API

type API struct {
	Name        string `` /* 136-byte string literal not displayed */
	Version     string `yaml:"version,omitempty" json:"version,omitempty" jsonschema_description:"API version segment, for example v1."`
	BasePath    string `` /* 130-byte string literal not displayed */
	Description string `yaml:"description,omitempty" json:"description,omitempty" jsonschema_description:"What this API is for."`
	// Permissions decides whether generated handlers check one.
	//
	// derived, the default, gives every endpoint a permission taken from its
	// resource and its operation — note.read, note.write, note.delete — and
	// generates the check. An authenticated caller with no grants then reaches
	// nothing, which is the right posture and a real behaviour change for a
	// project that had none.
	//
	// none generates no checks, for a project with no authorization at all.
	// `public:` on a resource or an endpoint is the per-endpoint escape hatch and
	// works either way.
	Permissions PermissionMode `` /* 226-byte string literal not displayed */

	SearchMethod SearchMethod `` /* 210-byte string literal not displayed */

	// RevisionHeader carries the revision a client was built against, and the
	// one the server was generated from on the way back.
	//
	// It is configurable because it is a name in a namespace rig does not own:
	// a gateway in front of the server may already mean something else by it.
	RevisionHeader string `` /* 170-byte string literal not displayed */

	// OpenAPI says whether the generated document is a route as well as a file.
	OpenAPI APIOpenAPI `` /* 162-byte string literal not displayed */
}

API configures the generated HTTP surface.

type APIOpenAPI added in v0.3.0

type APIOpenAPI struct {
	// Serve mounts the document beside the routes it describes, at
	// <base_path>/openapi.json and <base_path>/openapi.yaml.
	//
	// Nothing else to write. An embed directive resolves against the directory of
	// the file it is written in and cannot climb out of it, and the document is
	// written to the openapi generator's out_dir rather than beside the router —
	// so that generator writes the embed beside the document, in a package of its
	// own, and the router imports it at `project.module` joined to that out_dir
	// read from the module root, which the nearest go.mod is what locates.
	//
	// What it costs is that the directory becomes a Go package: its name has to
	// be one Go would accept, and it has to be inside the module the router is
	// generated into. RIG3011 is where both are said.
	Serve bool `` /* 221-byte string literal not displayed */
}

APIOpenAPI is where the generated document is reachable from.

This block is about routes. Which renderings are written, and where they are written to, is the `openapi` generator's own `formats` option and `out_dir` — that block is about files. The two are easy to confuse and do different things: turning `serve` on without the generator configured leaves nothing to serve, which is what RIG3011 says.

type Auth

type Auth struct {
	// Enabled says this project authenticates its callers.
	//
	// Off by default, and deliberately explicit: a project with an `auth:` block
	// it never turned on is easier to spot than one that started mounting a
	// sign-in endpoint because a file appeared. It is what makes `server-go`
	// write the wiring at all, and everything else in this block is read only
	// when it is set.
	Enabled bool `` /* 185-byte string literal not displayed */

	// BasePath prefixes the endpoints. The provider routes sit under it, at
	// <base>/oauth/{provider}/start.
	BasePath string `` /* 144-byte string literal not displayed */

	Tenant    AuthTenant    `yaml:"tenant,omitempty" json:"tenant,omitempty" jsonschema_description:"How a request names the tenant it is for."`
	Session   AuthSession   `` /* 126-byte string literal not displayed */
	EmailCode AuthEmailCode `` /* 192-byte string literal not displayed */
	Limits    AuthLimits    `` /* 138-byte string literal not displayed */

	// AllowTenantCreation mounts POST <base>/tenants, where somebody signed in
	// makes one and becomes its Owner. The policy — who may, what a name may be,
	// what else a new tenant needs — stays in Go, because each of those is a
	// function.
	AllowTenantCreation bool `` /* 201-byte string literal not displayed */

	// RequireVerifiedEmail refuses a sign-in until the address is verified.
	RequireVerifiedEmail bool `` /* 158-byte string literal not displayed */

	// AllowedIdentityDomains restricts who may become a person in this
	// deployment. Empty, the default, restricts nobody.
	//
	// It is the declarative half of `AllowIdentity` on the generated Hooks, and
	// it generates one: a gate that refuses a stranger whose address is outside
	// the list, on every door at once — a provider sign-in and a mailed code
	// alike. An application that sets the hook itself wins wholesale, because
	// two rules merged is a rule nobody can read.
	//
	// Two things about it that are not obvious from the name.
	//
	// It is **not** `rig_tenant.allowed_email_domains`, which is a different
	// question at a different time: that one governs whether somebody may hold
	// an account in a particular tenant, and it is per tenant and set at
	// runtime. This one governs whether they may exist here at all, and a
	// sign-in that names no tenant — the ordinary browser flow — never reaches
	// the other.
	//
	// And an administrator is not asked. An invitation or a direct provision
	// goes through whatever this says, because somebody who is already here
	// typed that address in, and a deployment that could not invite an auditor
	// or a contractor would be one nobody could run.
	AllowedIdentityDomains []string `` /* 440-byte string literal not displayed */

	// TrustedProxies are the networks whose X-Forwarded-For may be believed.
	//
	// Empty means none, and that is deliberate: an address read from a header a
	// client controls is an address a client chooses, and a rate limit keyed on
	// one is a rate limit an attacker walks around.
	TrustedProxies []string `` /* 192-byte string literal not displayed */

	// LogRetention is how long an entry in rig_auth_log is kept. Zero, the
	// default, keeps everything.
	//
	// Keeping everything is the honest default rather than the good one: nothing
	// prunes this table today, and the alternative — rig choosing a window — would
	// be rig deciding how long somebody's compliance obligation runs.
	//
	// It cannot be shorter than the longest rate limit window, and that is refused
	// rather than clamped. This table is what the limiter counts, so deleting rows
	// inside a window clears the lockout they were adding up to: a limiter that
	// silently stops limiting, which is the failure nobody notices until it is
	// being exploited.
	LogRetention Duration `` /* 285-byte string literal not displayed */

	OAuth AuthOAuth `` /* 144-byte string literal not displayed */

	// Expose names foundation tables to generate for anyway.
	//
	// It adds the model, the repository and whatever API the table's
	// configuration asks for — for an administration screen listing the people
	// in a tenant, most often. `rig setup-project --expose rig_account` writes the
	// configuration to go with it.
	//
	// It changes nothing about authentication: the auth package still reaches
	// these tables through its own queries, and a generated repository beside
	// them is a second door into the same rows, which is exactly why the
	// scaffolded configuration keeps `rig_account` free of a Create.
	Expose []string `` /* 143-byte string literal not displayed */

	// Own treats the foundation as ordinary tables and generates for all of it.
	//
	// For a project that has taken the schema over — forked the migrations,
	// stopped importing rig/auth, and now maintains the tables itself. It is a
	// one-way door in practice: the generated repositories will not enforce what
	// the auth package enforces, so a password reaching a table through one of
	// them is a password nobody hashed.
	Own bool `` /* 176-byte string literal not displayed */
}

Auth is the authentication foundation: whether this project has one, how it behaves, and how its tables are treated.

Everything that is a fixed choice lives here rather than in a Go literal, and that is the point of the block. The generated wiring, the emitted specification and a client library all read the same numbers, so how long a token lives is documented and typed rather than being a value one file happens to pass. What is left for Go is what YAML cannot hold: a function, and a secret.

The table half needs no configuration and is what almost every project wants: rig generates nothing for the tables `rig setup-project` created, because their Go types, their stores and their endpoints already exist in the rig/auth module. A project is recognised as having them by its own migration files, so a table called `rig_account` that nobody scaffolded is an ordinary table and keeps getting a model and a repository like any other.

func (Auth) Configured

func (a Auth) Configured() bool

Configured reports whether the authentication block says anything beyond how the foundation's tables are treated.

`expose` and `own` are about generation and mean something in a project with no authentication at all, so they do not count as having configured one.

func (Auth) IR

func (a Auth) IR() *ir.Auth

IR projects the configuration into the document's own shape.

It returns nil for a project with no authentication, which is what tells a generator there is nothing to describe. Every value is already resolved, so this is a translation and not a second place where defaults are decided.

type AuthEmailCode added in v0.6.0

type AuthEmailCode struct {
	// Enabled mounts POST <base>/email-code and POST <base>/email-code/verify.
	// Off means the routes do not exist rather than answering 403, so there is
	// nothing to probe.
	Enabled bool `` /* 140-byte string literal not displayed */

	// Length is how many digits. Default 6.
	Length int `` /* 248-byte string literal not displayed */
	// TTL is how long a code lasts. Default 10m.
	TTL Duration `` /* 131-byte string literal not displayed */
	// MaxAttempts is how many wrong guesses kill a code. Default 3.
	//
	// A ceiling on one code, not a rate limit on an address, and the two are not
	// substitutes: a limit counts failures over a rolling window, so a fresh
	// code would arrive with the old code's failures still against it and five
	// mistypes would lock the address rather than killing one code.
	MaxAttempts int `` /* 330-byte string literal not displayed */

	// AllowProvisioning sends a code to an address rig has never seen, creating
	// the person when the code is asked for.
	//
	// Off by default, which makes the deployment invite-only: a code goes only
	// to an address that already has an identity. On, it is self-registration,
	// and `auth.oauth.allow_provisioning` is the same decision for the other
	// door.
	AllowProvisioning bool `` /* 234-byte string literal not displayed */
}

AuthEmailCode is the mailed-code sign-in: rig sends a short numeric code to an address and somebody types it back.

It is the way in for anybody with no account at a configured provider, and in a deployment with no provider it is the only one. Off by default, because whether a product wants it is a decision and because it cannot work without a Notifier — which is Go, not YAML, so rig refuses the combination at startup rather than here.

type AuthLimit

type AuthLimit struct {
	Max    int      `yaml:"max,omitempty" json:"max,omitempty" jsonschema:"minimum=1" jsonschema_description:"How many are allowed in one window."`
	Window Duration `yaml:"window,omitempty" json:"window,omitempty" jsonschema_description:"How long the window is."`
}

AuthLimit is one limit: how many, and over how long. Either field left at zero takes rig's default for that limit.

func (AuthLimit) IR

func (l AuthLimit) IR() ir.AuthLimit

IR projects one limit.

type AuthLimits

type AuthLimits struct {
	// LoginByEmail locks one account after a handful of wrong codes.
	// Default 5 per 15m, cleared by a success.
	LoginByEmail AuthLimit `` /* 173-byte string literal not displayed */
	// LoginByIP throttles one source spraying many accounts. Default 50 per 15m.
	//
	// Deliberately looser than the address limit and deliberately not cleared by
	// a success: an email-only limit lets an attacker lock a victim out on
	// purpose, and an IP-only limit lets a botnet spray one guess everywhere.
	LoginByIP AuthLimit `` /* 222-byte string literal not displayed */

	// EmailCodeRequest bounds code requests per address. Default 5 per 1h.
	EmailCodeRequest AuthLimit `` /* 152-byte string literal not displayed */
	// EmailCodeByIP bounds code requests per source, across every address.
	// Default 100 per 1h.
	//
	// The limit that matters most when auth.email_code.allow_provisioning is
	// set: asking for a code then writes a person, and without this one script
	// makes ten thousand identities.
	EmailCodeByIP AuthLimit `` /* 243-byte string literal not displayed */
	// VerificationResend bounds verification mail per address. Default 5 per 1h.
	VerificationResend AuthLimit `` /* 152-byte string literal not displayed */
	// Refresh bounds one session's rotations. Default 60 per 1m: a client
	// refreshing sixty times a minute is looping, not working.
	Refresh AuthLimit `` /* 182-byte string literal not displayed */
	// APIKeyFailures bounds wrong keys per key. Default 20 per 1m.
	APIKeyFailures AuthLimit `` /* 155-byte string literal not displayed */
	// InvitationPreview bounds reading an invitation link per source. Default
	// 60 per 1h.
	//
	// The one unauthenticated endpoint keyed by a secret rather than by an
	// address, so what this bounds is one source walking the token space.
	InvitationPreview AuthLimit `` /* 164-byte string literal not displayed */
}

AuthLimits are the rate limits. Zero in any of them means rig's own default.

They are counted in the database rather than in memory, so two replicas cannot disagree about how many times a code has been tried and a restart does not clear somebody's lockout.

func (AuthLimits) LongestWindow

func (l AuthLimits) LongestWindow() (ir.Duration, string)

LongestWindow is the furthest back any of these limits counts, and the configuration key it belongs to.

The key rather than the runtime limit's name, because the answer goes into a diagnostic about a file somebody is editing: "auth.limits.password_reset" is something to go and look at, and "password.reset" is a string they would have to map back to one.

type AuthOAuth

type AuthOAuth struct {
	// BaseURL is this application's own origin. The callback URL is built from
	// it, and a provider compares that exactly.
	BaseURL string `` /* 216-byte string literal not displayed */
	// BaseURLEnv names an environment variable that overrides BaseURL, which is
	// the ordinary case: the origin differs per deployment and the file does not.
	BaseURLEnv string `` /* 236-byte string literal not displayed */
	// OriginFromHost derives the origin per request from the Host instead.
	//
	// What an application serving a tenant per subdomain needs: the state cookie
	// carrying the PKCE verifier is set on the host the sign-in started at, and a
	// browser will not send it to a sibling subdomain — so the callback URL is per
	// host, and every one of them is registered with the provider.
	OriginFromHost bool `` /* 247-byte string literal not displayed */

	// SigningKeyEnv names the environment variable holding the key that signs the
	// state parameter. Default OAUTH_SIGNING_KEY.
	//
	// A random key is generated when the variable is empty, which is fine for one
	// process and wrong for several: a callback may arrive at a different replica
	// than the one that started the flow.
	SigningKeyEnv string `` /* 272-byte string literal not displayed */

	// StateTTL bounds how long a sign-in round trip may take. Default 10m:
	// generous for a redirect, short enough that a stolen state is useless.
	StateTTL Duration `` /* 205-byte string literal not displayed */

	// AllowProvisioning creates an account the first time somebody signs in with
	// a provider.
	//
	// Off by default: a provider authenticates anybody, so joining a tenant stays
	// a decision. Without it a provider sign-in works only for an address that
	// already has an account.
	AllowProvisioning bool `` /* 241-byte string literal not displayed */

	// AllowJoining creates an account in the tenant a sign-in named, for
	// somebody who is not in it yet. Unset follows allow_provisioning, which is
	// what one key did when it gated both doors.
	//
	// Separate because the two are separate decisions and a deployment can want
	// opposite answers. "A provider may create a person, but only an invitation
	// may put them in a tenant" is the ordinary one: where the tenant comes
	// from a request, the join is the half a crafted /start link would abuse,
	// and an identity on its own reaches nothing.
	//
	// Read only by a sign-in that named a tenant. Where none was named, joining
	// is the picker's job rather than the callback's.
	AllowJoining *bool `` /* 275-byte string literal not displayed */

	// AllowedReturnTo bounds where a finished sign-in may land. An open redirect
	// is the classic mistake here.
	//
	// Origins, each scheme://host with nothing after the host — not paths. A
	// relative path is always allowed and is never matched against this list, so
	// an entry that is one does nothing at all, and neither does one with a
	// trailing slash. Both are refused rather than accepted and ignored.
	AllowedReturnTo []string `` /* 351-byte string literal not displayed */

	// Insecure allows the state cookie over plain HTTP, for local development
	// where a browser would refuse the __Host- prefixed one. Never anywhere real.
	Insecure bool `` /* 176-byte string literal not displayed */

	// Providers are the identity providers to offer.
	Providers []AuthProvider `` /* 140-byte string literal not displayed */
}

AuthOAuth is provider sign-in.

An empty provider list mounts nothing, so this block costs nothing to leave in place while a project decides.

type AuthProvider

type AuthProvider struct {
	// Name selects the provider, and is also the route segment: a provider called
	// google is reached at <base>/oauth/google/start.
	Name string `` /* 199-byte string literal not displayed */

	// ClientIDEnv and ClientSecretEnv default to the provider's name in upper
	// case: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
	ClientIDEnv     string `` /* 164-byte string literal not displayed */
	ClientSecretEnv string `` /* 180-byte string literal not displayed */

	// TenantEnv is Microsoft's own idea of a tenant, which has nothing to do with
	// rig's: "common" accepts any account, "organizations" excludes personal
	// ones, and a directory id restricts sign-in to one organization. Empty means
	// common. Default MICROSOFT_TENANT.
	TenantEnv string `` /* 251-byte string literal not displayed */

	// Required fails startup when the credentials are absent.
	//
	// The default skips the provider instead, which is what makes one
	// configuration work in both places: set the pair and the button appears, set
	// nothing and it does not.
	Required bool `` /* 211-byte string literal not displayed */
}

AuthProvider is one identity provider.

Credentials are named, never carried: a client secret in rig.yaml is a client secret in version control. The file says which environment variable holds it and the generated code reads that, which is also what lets one binary offer Google in production and nothing at all on a laptop.

type AuthSession

type AuthSession struct {
	// AccessTTL is the lifetime of the token that travels on every request.
	// Shorter is safer and refreshes more often. Default 10m.
	AccessTTL Duration `` /* 202-byte string literal not displayed */
	// RefreshTTL is how long an ordinary session lasts. Default 12h.
	RefreshTTL Duration `` /* 135-byte string literal not displayed */
	// RememberTTL is how long one lasts when somebody asked to stay signed in.
	// Default 30d.
	RememberTTL Duration `` /* 165-byte string literal not displayed */

	// RotationLeeway is how long a refresh token stays usable after it has been
	// exchanged. Default 30s.
	//
	// Without one, a dropped response is a logout: the client never received the
	// new pair, retries with the old one, and has its whole family revoked for a
	// network blip. Longer forgives more retries and widens the replay window.
	RotationLeeway Duration `` /* 283-byte string literal not displayed */

	// IdentityTTL is the lifetime of the tenant-less credential somebody holds
	// between signing in and picking a tenant. Default 30m.
	IdentityTTL Duration `` /* 186-byte string literal not displayed */
}

AuthSession is how long the things somebody holds stay good for.

Every value is a trade rather than a preference, so each one says what it costs. Zero means rig/auth's own default.

type AuthTenant

type AuthTenant struct {
	// From lists the sources to try, in order, and the first that names a tenant
	// wins. Default: header.
	//
	// A request that names none is not an error — a single sign-in page cannot
	// know which tenants an address belongs to before the password has been
	// checked — so the sources are consulted and their silence is an answer.
	//
	//   header  a header, X-Tenant-Id by default
	//   host    the leftmost label of the Host, looked up as a tenant slug
	//   query   a query parameter, for a local demonstration
	//   hook    the application's own resolver, handed to the generated wiring
	//
	// host is what a real deployment uses, and the only one that survives a
	// provider redirect: a callback URL is registered with the provider and fixed,
	// so it carries no header and no parameter of yours. A host does.
	From []string `` /* 207-byte string literal not displayed */

	// Header is what the header source reads. Default X-Tenant-Id.
	Header string `` /* 129-byte string literal not displayed */
	// Query is what the query source reads. Default tenant.
	Query string `` /* 130-byte string literal not displayed */

	// DefaultSlugEnv names an environment variable holding the slug to fall back
	// to when the host names no tenant.
	//
	// It exists for one real problem: Google and Microsoft will not register a
	// plain-http redirect URI for anything but localhost, so the subdomain a
	// host-based deployment is built around cannot be used on a laptop. Naming a
	// tenant for the host that has no subdomain is what lets a real sign-in be
	// tried there.
	DefaultSlugEnv string `` /* 177-byte string literal not displayed */
}

AuthTenant says how a request names the tenant it is for.

It is asked only where the tenant cannot be known some other way: a sign-in, a password reset, the start of a provider flow. Everything else takes its tenant from the token, which is why getting this wrong shows up at the front door rather than everywhere.

func (AuthTenant) Uses

func (t AuthTenant) Uses(source ir.AuthTenantSource) bool

Uses reports whether a tenant source is configured.

type Cache

type Cache struct {
	// Enabled is what turns the caching on. Off by default: a cache is a second
	// answer to a question the database was already answering correctly, and
	// switching that on for somebody is not an upgrade.
	Enabled bool `` /* 283-byte string literal not displayed */

	// TTL is how long an answer may be reused when no invalidation arrives, and
	// the honest statement of what the cache costs.
	TTL Duration `` /* 250-byte string literal not displayed */

	// Channel is the Postgres channel invalidations travel on.
	Channel string `` /* 260-byte string literal not displayed */

	// MaxEntries bounds each cache.
	MaxEntries int `` /* 165-byte string literal not displayed */
}

Cache is the invalidation channel cached reads are withdrawn over.

It is the one place rig keeps shared state outside Postgres, and the block exists because that departure needs a decision rather than a default. What it switches on is not a time-to-live — a bare one over authentication would mean a revoked session that keeps working, which is a trade rig refuses — but a Postgres NOTIFY issued inside the transaction that revoked something, so every replica forgets at the moment the revocation commits. The time-to-live below is the backstop for a notification nobody received.

Nothing an application writes reads this block. Every cache it turns on is held, filled and invalidated by generated code — inside rig/auth for the authenticated path, and inside the generated repository for a table that asked for `cache: true` — so there is no map to wire and no publish to remember. See `runtime/cache` for the argument, and NEXT.md for why the one read this does *not* cover is the application's own Grants function.

A table opting in is the one place that stops being airtight, and it is why opting in is per table rather than implied by this block: rig publishes the withdrawal from the writes it makes, so a write that goes around the generated repository serves a stale row until the entry expires.

func (Cache) Configured

func (c Cache) Configured() bool

Configured reports whether the block says anything beyond being switched on.

func (Cache) IR

func (c Cache) IR() *ir.Cache

IR is the resolved block, as a document carries it.

Nil for a project that caches nothing, so a generator asks the document one question rather than reading a flag and deciding what it implies.

type Config

type Config struct {
	Version int `yaml:"version,omitempty" json:"version,omitempty" jsonschema_description:"Configuration format version."`

	Project    ProjectInfo `yaml:"project" json:"project" jsonschema_description:"Identity of the application being generated."`
	Layout     Layout      `yaml:"layout,omitempty" json:"layout,omitempty" jsonschema_description:"Where table configuration and generated code live."`
	API        API         `yaml:"api,omitempty" json:"api,omitempty" jsonschema_description:"Shape of the generated HTTP API."`
	Servers    Servers     `` /* 239-byte string literal not displayed */
	Web        Web         `` /* 227-byte string literal not displayed */
	Database   Database    `` /* 126-byte string literal not displayed */
	Migrations Migrations  `yaml:"migrations,omitempty" json:"migrations,omitempty" jsonschema_description:"Migration file location."`
	Auth       Auth        `yaml:"auth,omitempty" json:"auth,omitempty" jsonschema_description:"How the authentication foundation's tables are treated."`
	Files      Files       `` /* 128-byte string literal not displayed */

	Notifications Notifications `` /* 134-byte string literal not displayed */
	Presence      Presence      `yaml:"presence,omitempty" json:"presence,omitempty" jsonschema_description:"Who is here, and what they are looking at."`
	Throttle      Throttle      `` /* 188-byte string literal not displayed */
	Cache         Cache         `` /* 217-byte string literal not displayed */

	Tracing Tracing `` /* 128-byte string literal not displayed */

	Monitoring Monitoring `` /* 147-byte string literal not displayed */

	Naming   Naming   `yaml:"naming,omitempty" json:"naming,omitempty" jsonschema_description:"How database names become Go and JSON names."`
	Validate Validate `yaml:"validate,omitempty" json:"validate,omitempty" jsonschema_description:"Severity of each configurable convention rule."`

	Generators []Generator `yaml:"generators,omitempty" json:"generators,omitempty" jsonschema_description:"Generators to run, in order."`
}

Config is the contents of rig.yaml.

func (*Config) ConfiguredWeb added in v0.5.1

func (c *Config) ConfiguredWeb() bool

ConfiguredWeb reports whether the project named a front end at all, which is the switch every generated cross-origin line hangs off.

type Database

type Database struct {
	Image         string `yaml:"image,omitempty" json:"image,omitempty" jsonschema_description:"Container image for the throwaway database."`
	ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty" jsonschema_description:"Name of the throwaway container."`
	Port          int    `yaml:"port,omitempty" json:"port,omitempty" jsonschema_description:"Host port the throwaway database listens on."`
	Name          string `yaml:"name,omitempty" json:"name,omitempty" jsonschema_description:"Database name."`
	User          string `yaml:"user,omitempty" json:"user,omitempty" jsonschema_description:"Database user."`
	Password      string `` /* 162-byte string literal not displayed */
	Schema        string `yaml:"schema,omitempty" json:"schema,omitempty" jsonschema_description:"Postgres schema to read. Defaults to public."`

	// Settings are extra server parameters for the throwaway container, passed
	// as -c flags. electric.enabled adds wal_level=logical when nothing lists
	// one, because logical replication cannot be turned on after the server has
	// started.
	Settings []string `` /* 169-byte string literal not displayed */

	// Electric is a sync-service container `rig db up` manages beside the
	// database. Nested here rather than top-level because its lifecycle is the
	// database container's: up starts it, down stops it, reset removes it.
	Electric DatabaseElectric `` /* 153-byte string literal not displayed */

	// URL, when set, is used as-is and no container is started.
	URL string `` /* 157-byte string literal not displayed */
}

Database says where rig runs migrations and reads the schema.

With no URL, rig starts a throwaway container, applies the migrations, reads the schema back, and leaves the container running for the next command. Set URL to point at a database you manage instead, which is what CI does.

type DatabaseElectric

type DatabaseElectric struct {
	Enabled       bool   `` /* 136-byte string literal not displayed */
	Image         string `yaml:"image,omitempty" json:"image,omitempty" jsonschema_description:"Container image for the sync service."`
	ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty" jsonschema_description:"Name of the sync-service container."`
	Port          int    `yaml:"port,omitempty" json:"port,omitempty" jsonschema_description:"Host port the sync service listens on."`
}

DatabaseElectric is the sync service `rig db up` runs beside the database.

It is a different thing from the per-table `electric:` key, which says a table has live-sync shapes, and from server-go's `electric_url` option, which says where the generated proxy forwards to — that URL should agree with the port here.

type Duration

type Duration time.Duration

Duration is a length of time in a configuration file: 10m, 12h, 30d, 500ms.

Go's own syntax, extended with d for days, because a refresh token that lasts a month is written 30d by everybody who writes it down. See github.com/simonjanss/rig/pkg/ir.ParseDuration.

func (Duration) Duration

func (d Duration) Duration() time.Duration

Duration returns the value as a time.Duration.

func (Duration) IR

func (d Duration) IR() ir.Duration

IR converts to the document's own duration type.

func (Duration) JSONSchema

func (Duration) JSONSchema() *jsonschema.Schema

JSONSchema describes a duration as the string it is.

Without this the reflector would see the underlying int64 and publish a schema that rejects "10m" — which is the only form anybody writes.

func (Duration) MarshalYAML

func (d Duration) MarshalYAML() (any, error)

MarshalYAML writes the string form, so a configuration file rig rewrites keeps reading the way somebody would have typed it.

func (Duration) String

func (d Duration) String() string

String renders the canonical form, largest unit first.

func (*Duration) UnmarshalYAML

func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML reads the string form.

type Files

type Files struct {
	// Enabled says this project accepts uploads. Off by default, and what makes
	// `server-go` write the wiring at all.
	Enabled bool `` /* 175-byte string literal not displayed */

	// Expose projects rig_file as a read-only resource.
	//
	// It is here rather than assumed because the row is the only place the file's
	// URL lives, and a client that cannot sync rig_file cannot use the column
	// that exists for it. Off leaves the table managed and unprojected, which is
	// right for a project whose files are only ever fetched through the download
	// route.
	//
	// There is no write path to find either way: the endpoints rig synthesizes
	// are the upload and the download, and a client that could POST a row with an
	// arbitrary storage key and no bytes has found a way around the rules.
	Expose bool `` /* 201-byte string literal not displayed */

	// Backend is where the bytes go: memory or s3.
	Backend string `` /* 204-byte string literal not displayed */

	S3 FilesS3 `yaml:"s3,omitempty" json:"s3,omitempty" jsonschema_description:"Bucket settings, read only when backend is s3."`

	// MaxBytes caps one upload. It is a hard per-file cap and not a quota: rig
	// does not do storage quotas, and saying so is better than implying one.
	MaxBytes int64 `` /* 184-byte string literal not displayed */

	// InlineTypes are the sniffed content types served without an attachment
	// disposition.
	//
	// Everything else downloads. The list is short on purpose: a file served
	// inline from the API origin runs there, and the URL is in a synced row that
	// will end up in an <img> or an <a> without anybody thinking about it.
	// text/html is the one that must never be on it.
	InlineTypes []string `` /* 247-byte string literal not displayed */

	// AbandonedAfter is how long a file row may sit with no bytes behind it
	// before the sweeper takes it.
	//
	// A row is inserted before the upload and finalized after it, so one with a
	// null uploaded_at is either in flight or the remains of a request that died.
	// This is how long it gets the benefit of the doubt.
	AbandonedAfter Duration `` /* 194-byte string literal not displayed */

	// RestoreWindow is how long a deleted file stays restorable, and therefore
	// how long its bytes outlive the delete.
	//
	// It lives here rather than in a table configuration because rig_file does
	// not have one — an asymmetry with `restore_window_days` worth knowing about
	// before going to look for the key. The key is refused on rig_file for the
	// same reason: this number is how long the bytes are kept as well as how
	// long the row can be brought back, and a second copy of it could only ever
	// disagree.
	//
	// A projected rig_file gets a generated restore endpoint, whose window is a
	// whole number of days — see [Files.RestoreWindowDays] — so with
	// `files.expose` set this has to be at least 24h.
	//
	// If the bucket has a lifecycle rule of its own, that rule has to outlive
	// this, or a restore inside the window succeeds and hands back a row pointing
	// at nothing. The S3 adapter refuses to start when it can tell that is the
	// case.
	RestoreWindow Duration `` /* 244-byte string literal not displayed */

	// CookieDownloads accepts the session cookie on file GET routes.
	//
	// A stored URL is stable and unsigned and sits in a synced row, which means
	// it exists to be used directly: <img src>, <a download>, a background-image.
	// None of those attach an Authorization header, and a bearer token is the
	// only credential rig's authentication otherwise understands.
	//
	// GET only, and that is not an oversight: widening it past GET reintroduces
	// exactly the cross-site request problem the bearer header avoids. It is
	// opt-in because an application that never renders a file in a browser should
	// not be carrying a cookie path at all.
	CookieDownloads bool `` /* 291-byte string literal not displayed */
}

Files is where uploaded bytes are kept, and what an upload may be.

It is configuration rather than a Go literal for the reason M5.8 settled for `auth:`: a byte cap or a sweep interval written in code is a number the generated documentation cannot quote, and a number nobody can find without reading the wiring. Everything with a fixed answer lives here, and it is resolved when the file is read, so the generated wiring, the specification and a client library all quote the same number rather than three zeros meaning "something else decides".

Zero means "left out" throughout, the way it does for every duration in this file. There is deliberately no way to write a zero that survives: an upload cap of nothing and a restore window of nothing are not configurations anybody wants, and reading them as "use the default" is kinder than a diagnostic.

func (Files) Configured

func (f Files) Configured() bool

Configured reports whether the files block says anything beyond how the managed table is treated, the same way Auth.Configured does.

func (Files) IR

func (f Files) IR() *ir.Files

IR is the resolved block, as a document carries it.

Nil for a project that accepts no uploads, so that a generator asks the document one question — is there file handling — rather than reading a flag and then five numbers that may or may not mean anything.

Every duration is resolved to seconds here. A document is JSON, and a Go duration in one is either a number nobody can read or a string every consumer has to parse.

func (Files) RestoreWindowDays

func (f Files) RestoreWindowDays() int

RestoreWindowDays is Files.RestoreWindow in whole days, which is the granularity a generated restore window is written in.

rig_file has no `restore_window_days` of its own — this is where the number the compiler puts on the resource comes from — so the conversion happens here rather than in a table configuration nobody writes.

Truncated rather than rounded up: the row has to stop being restorable before the bytes behind it go, and a window that outlived them would hand a caller back a row pointing at nothing.

type FilesS3

type FilesS3 struct {
	Bucket string `` /* 129-byte string literal not displayed */
	// BucketEnv names an environment variable holding the bucket, for a name
	// that differs per deployment.
	BucketEnv string `` /* 168-byte string literal not displayed */

	Region string `yaml:"region,omitempty" json:"region,omitempty" jsonschema_description:"Bucket region."`
	// Endpoint points at something other than AWS — MinIO, R2, a test double.
	Endpoint string `` /* 136-byte string literal not displayed */

	AccessKeyEnv string `` /* 167-byte string literal not displayed */
	SecretKeyEnv string `` /* 175-byte string literal not displayed */
}

FilesS3 names the bucket and how to reach it.

The credentials are environment variable names rather than values, the way the OAuth client secrets already are: rig.yaml is a file people commit.

type FoundationMode

type FoundationMode string

FoundationMode selects how rig's own tables reach the database.

rig owns a dozen tables — the identities, the tokens, the file rows, the notifications — and their DDL has to get there somehow. There are two readings and the difference is who keeps the file.

const (
	// FoundationVendored copies rig's migrations into the project's own
	// migrations directory, numbered into its sequence and recorded in its
	// bookkeeping table. It is the default, and the reason is readability: the
	// directory is then the whole truth about the database, and somebody can read
	// it at three in the morning without knowing which version of rig is
	// installed.
	//
	// Upgrades arrive as new files. rig's sets are append-only, so a project three
	// versions behind gets three migrations it can read the diff of before
	// applying.
	FoundationVendored FoundationMode = "vendored"

	// FoundationEmbedded leaves the DDL in the modules that own it — rig/auth,
	// rig/files, rig/notify — each applying and recording its own set. The
	// project's migrations directory then holds only the project's own schema.
	//
	// What it buys is that rig's tables stop being a thousand lines of somebody
	// else's SQL in the repository, and that an upgrade is a module version rather
	// than a file to copy. What it costs is that the migrations directory no
	// longer says what is in the database on its own.
	FoundationEmbedded FoundationMode = "embedded"
)

type Generator

type Generator struct {
	Name string `yaml:"name" json:"name" jsonschema_description:"Registered generator name, for example persist-go."`
	// OutDir overrides the generator's default output directory, relative to
	// the project root.
	OutDir string `yaml:"out_dir,omitempty" json:"out_dir,omitempty" jsonschema_description:"Output directory, relative to the project root."`
	// Options is validated against the generator's own schema, not this one.
	Options map[string]any `` /* 158-byte string literal not displayed */
}

Generator selects one generator to run and configures it.

Selection is by name rather than by output path, so adding a generator adds no fields to this struct.

type Layout

type Layout struct {
	TableDir   string `` /* 171-byte string literal not displayed */
	ConfigFile string `` /* 147-byte string literal not displayed */
}

Layout says where a table's configuration and code live.

The templates take {table} for the snake_case name, {Table} for PascalCase, and {tables} for the plural. Putting a table's configuration next to the service code that implements it keeps everything about one table in one directory; a flat layout is a one-line change.

type Migrations

type Migrations struct {
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty" jsonschema_description:"Directory holding goose migration files."`
	// Table is the goose bookkeeping table.
	Table string `yaml:"table,omitempty" json:"table,omitempty" jsonschema_description:"Name of the migration bookkeeping table."`
	// Foundation is who keeps rig's own migrations. See [FoundationMode].
	//
	// It is chosen once. The two modes record their history in different tables,
	// so a project that switches after it has a database would re-apply a schema
	// that is already there — which rig refuses to discover in psql, and says so
	// from `rig validate` instead.
	Foundation FoundationMode `` /* 332-byte string literal not displayed */
}

Migrations says where migration files live, and whose they are.

func (Migrations) Vendored

func (m Migrations) Vendored() bool

Vendored reports whether rig's own migrations live in this project's migrations directory. It is the default, so an unset mode is this one.

type Monitoring

type Monitoring struct {
	// Enabled says this project serves the page. Off by default, and off means
	// there is no listener rather than a port answering 404 — `server-go`
	// writes no wiring for a project without it, so the API package names no
	// page at all.
	Enabled bool `` /* 197-byte string literal not displayed */

	// Addr is where the page listens, as host:port — "127.0.0.1:9090", or
	// ":9090" for every interface. Required when [Monitoring.Enabled] is set.
	//
	// The page has its own listener and its own [net/http.Server] inside the
	// same binary, rather than a route on the API's mux, because the bind
	// address is the only boundary here that a client cannot talk its way
	// around. [Monitoring.Allow] is matched against the connection's own
	// address, which behind a load balancer is the balancer's, so it matches
	// everything or nothing; a port bound to loopback is enforced by the kernel
	// and says the same thing in every deployment.
	//
	// There is no default, deliberately. A default port is one two rig services
	// on a host would fight over, and a default interface is a decision about
	// who can reach a page that lists every path, request id and error cause
	// this server has seen — neither is rig's to make quietly.
	//
	// $RIG_MONITOR_ADDR overrides it at run time, for the reason the span
	// destination is an environment variable: where a thing listens is a
	// property of the deployment, and moving it should not need a regenerate.
	Addr string `` /* 251-byte string literal not displayed */

	// BasePath is where the page is mounted on its own listener, defaulting to
	// [DefaultMonitorBasePath].
	//
	// It has nothing to collide with now that the page is not on the API's mux,
	// so this is a prefix a reverse proxy in front of [Monitoring.Addr] can key
	// on, and the reason the default is kept is that it is the URL projects
	// already have.
	BasePath string `` /* 152-byte string literal not displayed */

	// MaxTraces is how many requests the page lists, newest first. Zero means
	// [DefaultMonitorMaxTraces].
	MaxTraces int `` /* 145-byte string literal not displayed */

	// MaxLogs is how many log lines the page reads, newest first. Zero means
	// [DefaultMonitorMaxLogs].
	//
	// The lines themselves are a run-time arrangement — a file the deployment
	// names and a handler the application tees in — and this is the one part of
	// them the project decides, for [MaxTraces]'s reason: how much of a page
	// somebody reads is a property of the page.
	MaxLogs int `` /* 142-byte string literal not displayed */

	// PasswordEnv names the variable the page reads its password from,
	// defaulting to [DefaultMonitorPasswordEnv]. With nothing in it the page
	// does not listen at all — the port is closed, not guarded — and the server
	// says so once at startup: a project running where there is no password to
	// give is a project that gets no page rather than one that gets an open
	// one.
	PasswordEnv string `` /* 218-byte string literal not displayed */

	// Password is the password itself, for a project that would rather write it
	// here than arrange an environment.
	//
	// It is accepted and it warns, because rig.yaml is checked in and this page
	// shows every path, request id and error cause the server has seen. Whether
	// that trade is worth making is the project's call — a throwaway staging
	// box and a production deployment are not the same decision — and rig makes
	// it once, out loud, rather than for you.
	Password string `` /* 183-byte string literal not displayed */

	// Allow is the addresses that may reach the page, as CIDR ranges or single
	// addresses. Empty, the default, allows any and leaves the password as the
	// only check.
	//
	// It narrows rather than replaces: an address that is not on the list is
	// answered 404 before the password is compared, so a scan learns nothing
	// and a leaked password is not enough on its own. There is deliberately no
	// way to have the list without the password — an allowlist keyed on the
	// connection's address is not a boundary behind a load balancer, where
	// every request arrives from the balancer, and that failure is silent and
	// total.
	//
	// [Monitoring.Addr] is the boundary that does hold in that deployment, and
	// this list is the layer above it: the port decides which networks can open
	// a connection at all, and this decides which addresses on those networks
	// are answered.
	//
	// It is matched against the connection's own address and never against a
	// forwarded header, for the reason [Auth.TrustedProxies] exists: an address
	// read from a header a client controls is an address a client chooses.
	Allow []string `` /* 331-byte string literal not displayed */
}

Monitoring is rig's own page over what this server wrote about itself: the last few hundred requests, what each of them spent its time on, and the log lines they produced.

It exists for the deployment too small to be worth a collector and a Grafana in front of it, which is most of them for most of their life. It is a reader and not a store — the span file `tracing:` already writes is the store — so turning it on costs a listener and an HTML asset, and no schema, no retention policy and nothing to run beside the server.

Which is also why it cannot be turned on alone: with no spans there is nothing to read, and that is refused when the file is read rather than discovered as a page that is permanently empty.

func (Monitoring) Configured

func (m Monitoring) Configured() bool

Configured reports whether anything in the block was set, so that a block somebody filled in and never enabled is refused rather than ignored.

func (Monitoring) IR

func (m Monitoring) IR(serviceName string) *ir.Monitoring

IR is the resolved block, as a document carries it.

Nil for a project with no page, so that a generator asks the document one question rather than reading a flag and deciding what it implies — the same shape Tracing.IR has, and for the same reason: that nil is what keeps the page, and the rig/observe import that serves it, out of a project that never asked for either.

type Naming

type Naming struct {
	JSONCase string `` /* 174-byte string literal not displayed */
	// Initialisms are added to the built-in list of acronyms that stay
	// uppercase in Go identifiers.
	Initialisms []string `` /* 153-byte string literal not displayed */
	// Plurals overrides derived plurals, keyed by the singular table name.
	Plurals map[string]string `` /* 157-byte string literal not displayed */
}

Naming tunes the conversion from database names to code and wire names.

type Notifications

type Notifications struct {
	// Enabled says this project has an inbox. Off by default, and what makes
	// `server-go` write the wiring and the compiler keep rig's two notification
	// tables in the schema at all.
	Enabled bool `` /* 172-byte string literal not displayed */

	// Expose projects rig_notification_recipient as a resource, so the inbox
	// gets the filter grammar, the sort keys and a generated client.
	//
	// Off is the ordinary case and still leaves a working inbox: the routes
	// under /notifications are hand-written, because the tables are the same in
	// every project and there is nothing for a generator to vary. This is for
	// the project those routes are not enough for — and both stay, because the
	// difference between them is the point.
	//
	// It does not decide whether the tables are in the schema. They are, as soon
	// as `enabled` is set, because a link table pointing at a table rig has
	// dropped is not a link table any more.
	Expose bool `` /* 208-byte string literal not displayed */

	// DefaultDigest is what an account gets on a channel it has never set a
	// preference for. The last step of the three-step resolution.
	DefaultDigest string `` /* 240-byte string literal not displayed */

	// ClaimTTL is how long a dispatcher's claim on a delivery is honoured before
	// another one may take it.
	//
	// It exists for crashes: a process that claimed a row and died leaves it
	// Pending with a stale claim, and the next dispatcher past it takes it once
	// this has elapsed. A clean shutdown gives its claims back immediately and
	// does not wait this out.
	//
	// The number that matters is the slowest channel's own timeout, which rig
	// cannot know. Shorter than that and every message is claimed twice under
	// ordinary load — at-least-once stops being a crash-recovery property and
	// becomes an everyday one. Longer and a crashed pod's queue sits idle for
	// that long while healthy replicas walk past it.
	ClaimTTL Duration `` /* 254-byte string literal not displayed */

	// SendTimeout bounds one call into a channel.
	//
	// It is the deadline on the context a channel is handed, and the answer to
	// the relationship ClaimTTL's own comment above calls the one
	// misconfiguration worth understanding: rig could not know the slowest
	// channel's timeout while the channel owned it, and now the channel is told
	// what it is. Shorter than ClaimTTL, necessarily — a send that may outlive
	// the lease protecting it is a send whose row somebody else has already
	// taken.
	//
	// A channel is the only outbound call rig does not make itself, which is why
	// this is the only such value in this file. It is also cooperative: a Sender
	// that ignores the deadline hangs its dispatcher anyway, and the interface
	// documentation says so.
	SendTimeout Duration `` /* 234-byte string literal not displayed */

	// MaxAttempts is how many times a delivery is tried before it is Failed and
	// stops being claimed.
	MaxAttempts int `` /* 224-byte string literal not displayed */

	// BackoffBase is the first retry delay. Each attempt doubles it, up to
	// BackoffCap.
	BackoffBase Duration `` /* 174-byte string literal not displayed */

	// BackoffCap is the longest one wait may be, which is what makes a long
	// schedule safe to have.
	//
	// The three retry numbers are one decision, and this is the one that decides
	// what the tail of the schedule looks like. Doubling from a minute fourteen
	// times is five days, so without a ceiling the late attempts are not retries
	// — they are a row nobody will look at again. Capped, the tail is a series of
	// hourly knocks, which is what somebody watching a provider's status page
	// would do by hand.
	BackoffCap Duration `` /* 197-byte string literal not displayed */

	// Retention is how long a read and deleted inbox line is kept before the
	// dispatch task prunes it, along with the notifications and link rows left
	// with nothing pointing at them.
	//
	// It has to outlive the longest digest window: a weekly digest under a daily
	// retention is a digest assembled from rows that were deleted before it ran,
	// and it would present as "the weekly mail is sometimes empty" rather than
	// as a configuration error.
	Retention Duration `` /* 217-byte string literal not displayed */
}

Notifications is the inbox and the machinery that fills it.

It is configuration rather than a Go literal for the reason `auth:` and `files:` are: a claim lease or a retention window written in code is a number the generated documentation cannot quote and nobody can find without reading the wiring. Everything with a fixed answer is here, resolved when the file is read.

Zero means "left out" throughout, and there is deliberately no way to write a zero that survives: a claim lease of nothing and a retention of nothing are not configurations anybody wants.

func (Notifications) Configured

func (n Notifications) Configured() bool

Configured reports whether the notifications block says anything beyond whether there is an inbox and how its tables are treated, the same way Files.Configured does.

func (Notifications) IR

func (n Notifications) IR() *ir.Notifications

IR is the resolved block, as a document carries it.

Nil for a project with no inbox, so that a generator asks the document one question — are there notifications — rather than reading a flag and then five numbers that may or may not mean anything.

Every duration is resolved to seconds here. A document is JSON, and a Go duration in one is either a number nobody can read or a string every consumer has to parse.

type PermissionMode

type PermissionMode string

PermissionMode selects whether authorization checks are generated.

const (
	// PermissionsDerived is the default: one permission per resource and action,
	// checked in the handler.
	PermissionsDerived PermissionMode = "derived"
	// PermissionsNone generates no checks. It is for a project that has no
	// authorization — a demonstration, or an API behind something else that does
	// the deciding.
	PermissionsNone PermissionMode = "none"
)

type Presence

type Presence struct {
	// Enabled says this project tracks presence. Off by default, and what makes
	// `server-go` write the wiring and the compiler keep rig_presence in the
	// schema at all.
	Enabled bool `` /* 175-byte string literal not displayed */

	// Expose projects rig_presence as a resource, so presence gets the filter
	// grammar, the sort keys and a generated client.
	//
	// Off is the ordinary case and still leaves presence working: the routes
	// under /presence are hand-written, because the table is the same in every
	// project and there is nothing for a generator to vary, and the live shape is
	// how a browser actually reads it. This is for the project those are not
	// enough for.
	//
	// Read-only either way. The scaffolded table configuration is
	// `operations: [Get, List]` and no more: a client that could POST one of
	// these could claim to be somebody else, and the heartbeat route exists
	// precisely so that it cannot.
	Expose bool `` /* 236-byte string literal not displayed */

	// TTL is how long a session stays present after its last heartbeat.
	//
	// It is the cost of the whole feature stated as one number. Short, and
	// somebody on hotel wifi flickers in and out of everybody else's view; long,
	// and a closed laptop goes on editing the title for that long.
	//
	// The long end is survivable because this is the *backstop*: an ordinary
	// close, a navigation or a tab going to the background sends a leave, so the
	// TTL only has to cover a crashed tab and a dead network. That is what makes
	// a minute affordable rather than five seconds.
	TTL Duration `` /* 212-byte string literal not displayed */

	// Heartbeat is how often a browser says it is still there, and it is
	// answered to the browser rather than configured in it.
	//
	// Every beat is one row changed, which every subscriber to the presence
	// shape hears about. So this is not a latency knob: it is the write rate and
	// the fan-out rate, multiplied by tabs. See docs/presence.md for the
	// arithmetic before lowering it.
	Heartbeat Duration `` /* 258-byte string literal not displayed */

	// Sweep is how often the in-process sweeper deletes rows past their window.
	//
	// It is not what makes somebody disappear — whoever is reading decides that,
	// against the clock, and within a second. This is what keeps the table, and
	// every new subscriber's first fetch, from growing forever.
	//
	// **There is no value here that turns the sweeper off**, and there was going
	// to be: a negative duration cannot be written in this file, because
	// [DurationPattern] does not admit a sign. That turned out to be the right
	// answer rather than a limitation to work around. Whether a process runs a
	// background loop is a line in its own main function, not a number in a
	// configuration file — the `notifications:` block has no key for it either,
	// and an operator who wants the cron job to own this simply does not call
	// Start on the sweeper. This key is how often it ticks when it is running.
	Sweep Duration `` /* 208-byte string literal not displayed */

	// Grace is how long past the TTL a row is kept before it is deleted.
	//
	// It exists so the two expiry mechanisms cannot disagree. A subscriber stops
	// drawing a row at the TTL and the sweeper deletes it at TTL plus this, so a
	// row is always invisible before it is gone — never the other way round,
	// which would be a row that came back when a slow client caught up.
	Grace Duration `` /* 243-byte string literal not displayed */
}

Presence is who is here, and what they are looking at.

It is configuration rather than a Go literal for the reason `auth:`, `files:` and `notifications:` are: a TTL is the number that decides how long a closed laptop goes on looking like somebody editing, and a number like that written in a main function is one the generated documentation cannot quote and nobody can find without reading the wiring.

Two of these values are also **answered to the browser**. The heartbeat response carries the TTL and the heartbeat interval, so changing either is a deploy of the server rather than a release of the front end — which is the whole reason they are not constants in the browser package.

Zero means "left out" throughout, and there is deliberately no way to write a zero that survives: a TTL of nothing is not a configuration anybody wants.

func (Presence) Configured

func (p Presence) Configured() bool

Configured reports whether the presence block says anything beyond whether there is presence and how its table is treated, the same way Notifications.Configured does.

func (Presence) IR

func (p Presence) IR() *ir.Presence

IR is the resolved block, as a document carries it.

Nil for a project without presence, so that a generator asks the document one question — is there presence — rather than reading a flag and then four numbers that may or may not mean anything.

Every duration is resolved to seconds here, for the reason Notifications.IR gives.

type Project

type Project struct {
	// Root is the directory holding rig.yaml. Every relative path in the
	// configuration is resolved against it.
	Root string
	// ConfigPath is the configuration file itself.
	ConfigPath string

	Config *Config
	Index  *yamlconf.Index
	// contains filtered or unexported fields
}

Project is a loaded configuration together with where it came from.

func Load

func Load(start string) (*Project, diag.List)

Load finds and reads the configuration for the project containing start.

func LoadFile

func LoadFile(path string) (*Project, diag.List)

LoadFile reads a specific configuration file.

func Parse

func Parse(path string, data []byte) (*Project, diag.List)

Parse reads configuration bytes as though they came from path.

func (*Project) At

func (p *Project) At(segments ...string) diag.Anchor

At returns the anchor for a dotted path within rig.yaml.

func (*Project) DatabaseURL

func (p *Project) DatabaseURL() string

DatabaseURL is the connection string for this project.

An explicit URL wins; otherwise one is built for the throwaway container, with the session time zone pinned to UTC — see [dockerdb.Config.URL] for why that matters and what it does not affect. A URL somebody wrote themselves is left exactly as written: quietly appending a parameter to a connection string is a good way to break one that already carries its own.

func (*Project) MigrationsDir

func (p *Project) MigrationsDir() string

MigrationsDir is the absolute path to the migration directory.

func (*Project) Namer

func (p *Project) Namer() *naming.Namer

Namer returns the naming rules for this project.

func (*Project) OpenAPIIR added in v0.3.0

func (p *Project) OpenAPIIR() *ir.OpenAPI

OpenAPIIR resolves where the generated document is served from, or nil for a project that keeps it a file.

Everything here is derived, and that is the point: the routes come from `api.base_path`, and the package the document is embedded in is the project's module path joined to the openapi generator's `out_dir`. Both ends are already written down, there is exactly one right answer, and an option to state it again would be an option whose only wrong value is a typo.

The join is not the plain one, and the difference is the whole of #128. `out_dir` is a path from the directory holding rig.yaml, and an import path is a path from the module root; those agree only when rig.yaml sits at the module root. So the directory is taken relative to the go.mod above it — see [Project.goModuleAt] — and a project whose module begins under api/ gets the import its compiler would. A project with no go.mod yet is the assumption unchanged, because `rig init` writes no go.mod and the tutorial reaches `go mod init` after the first generate.

It is here rather than in a generator for the reason [Project.checkDeprecatedServerOptions] is: the question spans an `api:` key, `project.module` and the `generators:` list, and the only place all three are readable is the project configuration. A generator sees its own options and the frozen document, neither of which can say where another generator writes.

The paths are not filled in here. They belong to the route namespace, which is computed once at freeze so that nothing else has to know how a base path is joined — or how a collision with a resource route is reported.

func (*Project) Path

func (p *Project) Path(rel string) string

Path resolves a project-relative path against the project root. An absolute path is returned unchanged.

func (*Project) Rel

func (p *Project) Rel(abs string) string

Rel expresses an absolute path relative to the project root, for reporting. Paths outside the project are returned unchanged.

func (*Project) SearchMethod

func (p *Project) SearchMethod() SearchMethod

SearchMethod returns the configured Search exposure.

func (*Project) Severity

func (p *Project) Severity(configured string, code diag.Code) diag.Severity

Severity resolves a configurable rule's severity. An unset rule uses the code's own default, and "off" reports nothing at all.

func (*Project) TableConfigPath

func (p *Project) TableConfigPath(table string) string

TableConfigPath is the absolute path to one table's configuration file.

func (*Project) TableConfigPaths

func (p *Project) TableConfigPaths() ([]string, error)

TableConfigPaths returns every existing table configuration file, sorted.

It globs the layout template rather than walking the tree, then keeps only the paths the template can actually reproduce. That second step matters: under the default layout of services/{table}/{table}.yaml a plain glob also matches services/lesson/notes.yaml, and treating an unrelated YAML file as table configuration produces a baffling error.

func (*Project) TableDir

func (p *Project) TableDir(table string) string

TableDir is the absolute directory for one table's configuration and code.

func (*Project) TableForConfigPath

func (p *Project) TableForConfigPath(path string) (string, bool)

TableForConfigPath recovers the table a configuration path belongs to.

The name is read out of the first {table} placeholder, and then the whole template is re-expanded and compared: a path only belongs to a table if the layout would have produced exactly that path for it. Re-expanding is what makes {Table} and {tables} fall out correctly without inverting them.

A layout with no {table} placeholder cannot be inverted, so every glob match is accepted and the filename check at load time catches any mismatch.

func (*Project) UsesContainer

func (p *Project) UsesContainer() bool

UsesContainer reports whether rig manages the database itself.

type ProjectInfo

type ProjectInfo struct {
	Name string `yaml:"name" json:"name" jsonschema_description:"Short name of the application."`
	// Module is the Go module path of the generated application. Generated
	// imports are built from it.
	Module string `yaml:"module" json:"module" jsonschema_description:"Go module path of the application, used to build generated import paths."`
}

ProjectInfo identifies the application.

type SearchMethod

type SearchMethod string

SearchMethod selects how the Search operation is exposed.

const (
	// SearchQuery exposes Search as HTTP QUERY only.
	SearchQuery SearchMethod = "query"
	// SearchPost exposes Search as POST to a /_search sub-path only.
	SearchPost SearchMethod = "post"
	// SearchBoth exposes QUERY with the POST sub-path as an alias, which is the
	// default: QUERY is the correct method for a read with a body, but some
	// intermediaries still reject methods they do not recognize.
	SearchBoth SearchMethod = "both"
)

type Server added in v0.3.0

type Server struct {
	// Name identifies the deployment, and becomes an identifier in every
	// generated SDK: production is Go's ServerProduction and TypeScript's
	// servers.production.
	//
	// Lower snake case, which is the one spelling that becomes an ordinary name
	// in both languages without a rule anybody has to remember.
	Name string `` /* 215-byte string literal not displayed */

	// URL is where that deployment answers, origin and any prefix it sits
	// behind. The API's own base_path is appended to it, so it is not repeated
	// here.
	URL string `` /* 197-byte string literal not displayed */

	// Description is one line about what this deployment is for. It reaches the
	// OpenAPI document and the doc comment beside the generated constant.
	Description string `` /* 205-byte string literal not displayed */

	// Default is the deployment a caller who names none gets. At most one entry
	// may claim it; with nobody claiming it the first entry is the default, so a
	// project naming a single deployment writes no marker at all.
	Default bool `` /* 199-byte string literal not displayed */
}

Server is one deployment.

type Servers added in v0.3.0

type Servers []Server

Servers are the deployments this API answers on: production, staging, a machine on somebody's desk.

It is a project block rather than a generator option because every generator that names a URL has the same question to answer, and three copies of the answer are three things to keep in step. The OpenAPI document, the Go client and the TypeScript client all read this one list, so a document saying the API is at api.example.com cannot ship beside an SDK pointing somewhere else — and the language added next gets the deployments without growing an option of its own, which is the argument Generator already makes for selecting generators by name.

A list rather than a map keyed by name, because the order is data: the first entry is where a documentation viewer sends its trial request, and a map decoded into Go arrives in whatever order it likes.

There is no url_env beside url, and the asymmetry with auth.oauth.base_url_env is deliberate. That one exists because a server reads its own origin when it starts. These are constants compiled into somebody else's program, which cannot see your deployment's environment — and the program wanting a different one passes its own URL, which every generated constructor still accepts.

func (Servers) IR added in v0.3.0

func (s Servers) IR() []ir.Server

IR is the resolved block, as a document carries it.

The default is resolved here rather than in each generator. With three consumers asking "which deployment does a caller who named none get", the rule that an unclaimed default falls to the first entry has to be written once — in the place that can also refuse two claims.

Nil for a project that named no deployment, so a generator asks the document one question — does this project say where it runs — rather than reading a list and then deciding what an empty one implies.

type Throttle

type Throttle struct {
	// Enabled is what makes `server-go` write the wiring. Off by default: an API
	// that starts refusing calls after an upgrade nobody asked for is worse than
	// one that does not limit yet.
	Enabled bool `` /* 184-byte string literal not displayed */

	// APIKey, Account and Tenant apply to a caller who said who they were.
	APIKey  AuthLimit `` /* 129-byte string literal not displayed */
	Account AuthLimit `` /* 139-byte string literal not displayed */
	Tenant  AuthLimit `` /* 214-byte string literal not displayed */

	// IP applies only to a caller who did not say who they were.
	IP AuthLimit `` /* 244-byte string literal not displayed */

	// Interval is how long one replica may hold its own count before publishing
	// it. It is the accuracy of the whole feature, stated as time.
	Interval Duration `` /* 260-byte string literal not displayed */

	// Routes are extra limits on particular routes, on top of the per-caller
	// ones above.
	Routes []ThrottleRoute `` /* 179-byte string literal not displayed */

	// Exempt are route patterns nothing applies to.
	Exempt []string `` /* 209-byte string literal not displayed */
}

Throttle is how many calls one caller may make.

It is fair-use limiting and the documentation says so in those words. What it stops is a client stuck in a retry loop, one tenant's batch job crowding out the others, scraping, and an API that fans out to something metered. What it does not stop is a volumetric attack: by the time a request is here it has already cost a connection, a handshake and a goroutine, and the defence for that lives in front of the application rather than inside it.

The numbers are configuration rather than a Go literal for the reason `auth.limits` is: they are answered to clients in the RateLimit-* headers and quoted in the generated documentation, so the number a caller is told is the number the server enforces.

func (Throttle) Configured

func (t Throttle) Configured() bool

Configured reports whether the block says anything beyond being switched on.

func (Throttle) IR

func (t Throttle) IR() *ir.Throttle

IR is the resolved block, as a document carries it.

Nil for a project that does not throttle, so a generator asks one question rather than reading a flag and then a dozen numbers that may mean nothing.

type ThrottleRoute

type ThrottleRoute struct {
	// Pattern is the route as the router registers it, for example
	// "POST /api/v1/todos". It is matched exactly against what net/http
	// reports, not against the request path.
	Pattern string   `` /* 204-byte string literal not displayed */
	Max     int      `` /* 131-byte string literal not displayed */
	Window  Duration `yaml:"window,omitempty" json:"window,omitempty" jsonschema_description:"How long the window is."`
}

ThrottleRoute is one route's own limit.

type Tracing

type Tracing struct {
	// Enabled says this project emits spans.
	Enabled bool `` /* 216-byte string literal not displayed */
}

Tracing says whether this project's generated code emits spans.

One key, and the two things somebody expects to find beside it are deliberately elsewhere.

The service name is the API's name. A project has already said what it is called, and a second name here could disagree with the first — which is the kind of thing nobody notices until two deployments show up in a collector under names neither of them recognises.

Where the spans go is not generated at all. The same binary runs on a laptop, in CI and in production, and only the last of those has a collector; a project that had to regenerate to stop exporting would be a project that exports from its test suite. That is an environment variable, or a field on observe.Config in the main function — see docs/observability.md.

What is left is the one thing that genuinely belongs in a file the generators read: whether the emitted code opens spans at all. Off means absent — no import of rig/observe, no otel in the go.mod, and not one span that calls a no-op.

func (Tracing) IR

func (t Tracing) IR(serviceName string) *ir.Tracing

IR is the resolved block, as a document carries it.

Nil for a project that asked for no spans, so that a generator asks the document one question — does this project trace — rather than reading a flag and then deciding what it implies. That nil is what keeps rig/observe, and with it OpenTelemetry, out of an application that never asked for either.

The service name comes from the project rather than from this block. There is no `service_name` key to disagree with `project.name`, which is the kind of disagreement nobody notices until two deployments arrive in a collector under names neither of them recognises.

There is no checkTracing beside this, and nothing in applyDefaults. With one key there is nothing to refuse and nothing to fill in: a `tracing:` block that is off is a block that says what every project without one says. A check that cannot fire is worse than no check, because it reads like coverage.

type Validate

type Validate struct {
	UnmentionedColumn    string `` /* 220-byte string literal not displayed */
	MissingComment       string `` /* 167-byte string literal not displayed */
	ForeignKeyNotIndexed string `` /* 180-byte string literal not displayed */
	TenantIndexLeading   string `` /* 188-byte string literal not displayed */
	BooleanPrefix        string `` /* 178-byte string literal not displayed */
	TimestampSuffix      string `` /* 372-byte string literal not displayed */
	DateSuffix           string `` /* 192-byte string literal not displayed */
	ForeignKeyNaming     string `` /* 186-byte string literal not displayed */
	CascadeDelete        string `` /* 173-byte string literal not displayed */
	MigrationFilename    string `` /* 191-byte string literal not displayed */
}

Validate sets the severity of each configurable convention rule. Every value is one of off, warn, or error. Structural rules are not listed here: a schema that breaks them cannot be generated from at all.

type Web added in v0.5.1

type Web struct {
	// Origin is where the front end is served, as scheme://host[:port] with
	// nothing after the host.
	Origin string `` /* 209-byte string literal not displayed */

	// OriginEnv names the environment variable the origin comes from, for a
	// deployment whose front end differs per environment.
	//
	// This exists here and deliberately not beside `servers[].url`, and the
	// asymmetry is the same one `auth.oauth.base_url_env` makes: this value is
	// read by *this* server as it starts, so it can see its own environment. A
	// server URL is a constant compiled into somebody else's program, which
	// cannot.
	OriginEnv string `` /* 248-byte string literal not displayed */

	// CallbackPath is the path on the front end that receives a finished
	// sign-in. Default /auth/callback.
	CallbackPath string `` /* 179-byte string literal not displayed */

	// CORS is what a preflight from that front end is answered with.
	CORS WebCORS `` /* 171-byte string literal not displayed */
}

Web is where this API's browser front end is served, when that is not this API.

A top-level block rather than a key under `auth.oauth`, because two different things need the same fact and only one of them is authentication. A finished provider sign-in has to redirect somewhere, and a preflight has to be answered for some origin — and a project serving a single-page application with no provider sign-in at all still has the second question. `servers:` is the precedent: a deployment fact that several generators read and none owns.

Presence of the block is the switch, so there is no `enabled`. A project that serves its front end from this same origin writes nothing here and gets no cross-origin code emitted at all.

func (Web) IR added in v0.5.1

func (w Web) IR(present bool) *ir.Web

IR is the resolved block, or nil for a project that named no front end.

Nil is the question a generator asks — does this project have a browser front end somewhere else — rather than an empty struct it has to interpret.

type WebCORS added in v0.5.1

type WebCORS struct {
	// AllowedOrigins are origins besides [Web.Origin] that may call this API —
	// an administrative front end, most often.
	//
	// Each is scheme://host[:port]. One leading `*.` label is allowed, so
	// `https://*.example.com` matches any single subdomain; a bare `*` is not,
	// because this API answers bearer credentials and a policy that admits every
	// origin is one somebody should have to write out.
	AllowedOrigins []string `` /* 223-byte string literal not displayed */

	// AllowedOriginsEnv names the environment variable that replaces the list
	// above, as a comma-separated string. Default CORS_ORIGINS.
	//
	// Replaces rather than extends, which is how `base_url_env` behaves: a
	// deployment overriding a value should not have to know what it is
	// overriding.
	AllowedOriginsEnv string `` /* 302-byte string literal not displayed */

	// MaxAge is how long a browser may cache a preflight. Default 10m, which is
	// as long as Safari will honour.
	MaxAge Duration `` /* 170-byte string literal not displayed */
}

WebCORS is the part of a cross-origin policy a project has to say out loud.

Which methods and headers are allowed is not here, because it is not a decision: it follows from what this API's own endpoints read and answer with, which the document already describes. A project maintaining its own list is how the header a live-sync cursor needs ends up missing, and a subscription that stops after one response is a bug nobody attributes to a CORS list.

Jump to

Keyboard shortcuts

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