store

package
v0.0.0-...-e645756 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 58 Imported by: 0

Documentation

Overview

Package store is the bex control plane: the Postgres-backed source of truth for the product's business entities (tenants, apps, domains + their mappings to Ory and Stripe) and the minimal API over them. It projects `apps` rows into App CRs (app.bex.co/v1alpha1) for the operator to execute — policy/intent lives here, mechanism stays in the operator (docs/ADR003-control-plane.md).

Index

Constants

View Source
const (
	AccountDeletionPending  = "pending"
	AccountDeletionCleaning = "cleaning"
	AccountDeletionIdentity = "identity"
	AccountDeletionDone     = "done"

	AccountWorkspaceDelete  = "delete"
	AccountWorkspaceLeave   = "leave"
	AccountWorkspaceBlocked = "blocked"
)
View Source
const (
	DefaultAuditPageSize = 20
	MaxAuditPageSize     = 1000
)

DefaultAuditPageSize/MaxAuditPageSize bound ListAuditEvents' page — Render's audit-specific limit param (default 20, maximum 1000; captured from the public OpenAPI's auditLogLimitParam, w4/m26 — note the cap is audit-specific, NOT the general core.MaxPageLimit of 100).

View Source
const (
	BillingHealthy    = "healthy"
	BillingGrace      = "grace"
	BillingEnforcing  = "enforcing"
	BillingEnforced   = "enforced"
	BillingRecovering = "recovering"
	BillingExcluded   = "excluded"
	BillingComped     = "comped"

	BillingOutcomeFailure = "failure"
	BillingOutcomeSuccess = "success"
)
View Source
const (
	BlueprintAutoSyncIntentPending   = "pending"
	BlueprintAutoSyncIntentClaimed   = "claimed"
	BlueprintAutoSyncIntentCompleted = "completed"
	BlueprintAutoSyncIntentFailed    = "failed"
)

Intent state constants for blueprint_auto_sync_intents.

View Source
const (
	BlueprintSyncStateCreated = "created"
	BlueprintSyncStateRunning = "running"
	BlueprintSyncStateSuccess = "success"
	BlueprintSyncStateError   = "error"
)

Blueprint sync state constants (Render's vocabulary).

View Source
const (
	BlueprintStatusCreated = "created"
	BlueprintStatusPaused  = "paused"
	BlueprintStatusInSync  = "in_sync"
	BlueprintStatusSyncing = "syncing"
	BlueprintStatusError   = "error"
)

Blueprint status constants (Render's vocabulary). The legacy 'active' value stored in pre-m62 rows is mapped to 'in_sync' at the service layer.

View Source
const (
	DatastoreKindPostgres = "postgres"
	DatastoreKindKeyValue = "keyvalue"
)

DatastoreKind separates the two managed datastore products. It is the CHECK constraint's vocabulary, and it decides which availability names an edge takes — Render says a Postgres is "unavailable" and a Key Value is "unhealthy", and bex matches that rather than normalizing it away.

View Source
const (
	// A member normally has one phone and perhaps a tablet/test install. Ten
	// leaves ample headroom while bounding the persistent dimension one viewer
	// can project into every push event.
	MaxActivePushDevicesPerSubject = 10
	// The workspace cap is a second, race-safe ceiling across many subjects. It
	// protects the global worker even if a workspace legitimately has a large
	// membership or several compromised members.
	MaxActivePushDevicesPerWorkspace = 1000
)
View Source
const (
	EnvGroupIdle           = "idle"
	EnvGroupBusy           = "busy"
	EnvGroupRepairRequired = "repair_required"
)

Content states of an environment group (env_groups.content_state).

View Source
const (
	EventStatusSucceeded = "succeeded"
	EventStatusFailed    = "failed"
	EventStatusCanceled  = "canceled"
)

Closed lifecycle-step outcomes for a *_ended fact's Status column — the same structural discipline as reason_code: a step outcome is never an arbitrary string. Mirror service_event_facts' status CHECK (migration 0057).

View Source
const (
	EventReasonImagePullBackoff = "image_pull_backoff"
	EventReasonReadinessFailed  = "readiness_failed"
	EventReasonRootDirectory    = "root_directory"
	EventReasonBuildFilter      = "build_filter"
	EventReasonSkipPhrase       = "skip_phrase"
	// EventReasonSuperseded is a deploy canceled because a newer release
	// replaced it (w4/089) — distinct from a user-initiated cancel, which
	// carries no reason code.
	EventReasonSuperseded = "superseded"
	// Why a cron run failed (w4/m114), one per field of the reason object on
	// Render's "Cron Job Run Ended" event.
	EventReasonNonZeroExit = "non_zero_exit"
	EventReasonOOMKilled   = "oom_killed"
	EventReasonEvicted     = "evicted"
	EventReasonTimedOut    = "timed_out"
)
View Source
const (
	EventSourceDeploy = "deploy"
	EventSourceAudit  = "audit"
	EventSourceFact   = "fact"
)

Event source discriminators — which table an event row was projected from.

View Source
const (
	EventPhaseStarted = "started"
	EventPhaseEnded   = "ended"
)

Deploy-event phases: one deploys row yields a started event at created_at and, once terminal, an ended event at finished_at.

View Source
const (
	GitWebhookReplayKeyManual = "manual"
	GitWebhookReplayKeyGitHub = "github"

	// MaxGitWebhookReplayClaimsPerScope bounds the durable rows one tenant can
	// allocate across all signing epochs that live API replicas still accept.
	// At roughly one row per push, 100k leaves years of ordinary headroom while
	// imposing a finite per-tenant storage cost. Reaching it fails closed until
	// a retired signing epoch is reclaimed or an operator intervenes.
	MaxGitWebhookReplayClaimsPerScope = 100_000

	// GitWebhookReplayMaintenanceInterval is each replica's epoch heartbeat and
	// retired-epoch sweep cadence.
	GitWebhookReplayMaintenanceInterval = time.Minute
	// GitWebhookReplayLeaseTTL is the failure-detection window before a crashed
	// or disconnected replica's signing epoch can be retired.
	GitWebhookReplayLeaseTTL = 5 * time.Minute
)
View Source
const (
	JobPending   = "pending"
	JobRunning   = "running"
	JobSucceeded = "succeeded"
	JobFailed    = "failed"
	JobCanceled  = "canceled"
)

Job status constants — the Render job status enum.

View Source
const (
	// RegimeLabel distinguishes a workspace's hosting namespace from its sandbox
	// namespace (ADR043 D2 — both untrusted, opposite default network postures).
	// RegimeLabel also rides OpenSandbox create metadata, selecting the resulting
	// BatchSandbox and Pod for sandbox-only network policy.
	RegimeLabel   = "app.bex.co/regime"
	RegimeHosting = "hosting"
	RegimeSandbox = "sandbox"

	// AppsQuotaCountKey is the per-namespace ResourceQuota count key that
	// enforces the per-workspace service cap (ADR043 D3), replacing the
	// retired BEX_MAX_SERVICES app-code check. Exported so the create path
	// (internal/apps) can map a quota-exceeded admission error back onto the
	// Render-shaped cap message (core.QuotaCapError).
	AppsQuotaCountKey = "count/apps.app.bex.co"
	// DatabasesQuotaCountKey / KeyValuesQuotaCountKey are the datastore siblings.
	// Live only since ADR043 D8 put the CRs in the namespace being charged
	// (w7/m77); before that these dimensions existed but counted nothing. The
	// postgres/keyvalue services map a rejection on these keys back to the same
	// Render-shaped message the service cap uses.
	DatabasesQuotaCountKey = "count/databases.app.bex.co"
	KeyValuesQuotaCountKey = "count/keyvalues.app.bex.co"
)

Per-tenant namespace isolation (ADR043). A workspace maps unconditionally to a hosting namespace `<ws>` and a `<ws>-sandbox` namespace (pillar 5, m32). The NamespaceReconciler is the lifecycle mechanism: it is level-triggered exactly like the App Reconciler — a full resync plus a Kick after workspace writes — so the set of tenant namespaces is a rebuildable projection of the `tenants` table, never imperative drift. Every namespace gets its identity labels, a plan-scaled ResourceQuota/LimitRange, and a default-deny NetworkPolicy; App workloads land in `<ws>`, per-`<ws>` RBAC binds there, and same-workspace/platform NetworkPolicy allows apply on top. The transitional BEX_TENANT_NAMESPACES/BEX_TENANT_SANDBOX_NAMESPACES rollout gates that used to make this opt-in were retired in w3/m34 once the fleet was fully migrated.

View Source
const (
	PlanHobby      = "hobby"
	PlanPro        = "pro"
	PlanScale      = "scale"
	PlanEnterprise = "enterprise"
)

Workspace plans mirror Render's post-2026-04-23 capability lineup (verified in .pm/w6/RESEARCH-workspaces.md): Hobby is capped, while higher plans lift member/service caps and add roles. Monthly workspace fees live in lego/backend/internal/pricing (ADR030): Render's flat subscriptions × 0.70 (Hobby $0, Pro $17.50, Scale $349.30; Enterprise custom). tenants.plan is still the capability vehicle; resource-tier usage is billed separately (ADR040, ADR046).

View Source
const (
	LabelManagedBy = "app.kubernetes.io/managed-by"
	ManagedByValue = "bex-controlplane"
	// ControlPlaneLabel refines LabelManagedBy from "a bex control plane owns
	// this" to "WHICH one owns it" (w6/m39). Both projectors here prune by
	// ABSENCE from their own database using a CLUSTER-scoped List, so without an
	// instance identity every bex-api holding a BEX_CP_DB_URI deletes every
	// managed object it cannot account for — including another control plane's.
	// That is not hypothetical: two `dev-N` harnesses sharing the CAPD mock
	// cluster deleted each other's tenant namespaces within one resync, observed
	// live in both directions (.pm/w3/017.md). Read it through ownedBy, never
	// directly, so the deliberate unlabeled-legacy asymmetry stays in one place.
	ControlPlaneLabel = "app.bex.co/control-plane"
	// DefaultControlPlaneIdentity is the identity an unconfigured control plane
	// runs under, so production behaves exactly as it did before w6/m39 and only
	// a deliberately-configured harness narrows its own prune scope.
	DefaultControlPlaneIdentity = "production"
	LabelAppID                  = core.LabelAppID
	// LabelTenant aliases core.LabelTenant — one label, one constant, so the
	// stamp (here) and the gate (core.Base.GetApp) can never drift apart.
	LabelTenant = core.LabelTenant
	// LabelWorkspace aliases core.LabelWorkspace so the stamp (here) and the
	// operator's propagation to pod templates share one canonical value.
	LabelWorkspace = core.LabelWorkspace
)

Labels stamped on every projected App CR. LabelAppID ties the CR back to its apps row (the reconciler's join key); LabelTenant carries the owning tenant's id (tea-<id>) — core.Base.GetApp's single source of truth for gating a fetched App against the caller's tenant, so every feature that fetches by name (apps/logs/metrics/secrets) inherits the same check; the managed-by label scopes list/delete so hand-applied Apps are never touched.

ManagedByValue is PERSISTED DATA — it lives on every projected App CR in live clusters. The service was renamed bex-backend, but changing this value would blind the projection to existing CRs (List filters on it) and make it re-Create them into name conflicts. Flip it only with a relabel migration. LabelTenant is re-stamped on every resync (stampLabels), so a value change (it once carried the tenant name) migrates existing CRs without a relabel.

View Source
const (
	DeployCreated             = "created"
	DeployQueued              = "queued"
	DeployBuildInProgress     = "build_in_progress"
	DeployBuildFailed         = "build_failed"
	DeployPreDeployInProgress = "pre_deploy_in_progress"
	DeployPreDeployFailed     = "pre_deploy_failed"
	DeployUpdateInProgress    = "update_in_progress"
	DeployLive                = "live"
	DeployUpdateFailed        = "update_failed"
	DeployDeactivated         = "deactivated"
	DeployCanceled            = "canceled"
)

Deploy status vocabulary — Render's complete eleven-state enum. The backend persists only states supported by current App/Job evidence; it never invents an unobserved intermediate phase merely to make the sequence look complete.

View Source
const (
	PreDeployRunning   = "running"
	PreDeploySucceeded = "succeeded"
	PreDeployFailed    = "failed"
	PreDeployCanceled  = "canceled"
)

Pre-deploy step status vocabulary — the deploy row's pre_deploy_status column (w1/m33). The lowercase projection of the App CR's status.preDeploy.Status, distinct from the overall deploy status: a deploy can be update_failed with pre_deploy_status 'failed' (its migration failed) or ” (its health check failed). Empty means no pre-deploy step ran. Canceled is the close's own verdict on a step still running when its deploy ended without going live (superseded or canceled, w4/187): no later observation can settle it. A pre_deploy_failed close settles such a step failed instead (w5/m114), and migration 0142's CHECK keeps any closed row from reading running.

View Source
const (
	TriggerCreate     = "create"      // the app's first deploy, opened by CreateApp
	TriggerAPI        = "api"         // an authenticated POST .../deploys
	TriggerDeployHook = "deploy_hook" // the App's unauthenticated secret-URL trigger
	TriggerRollback   = "rollback"    // a deploy created by Rollback (w2/m10), restoring an earlier image
	TriggerNewCommit  = "new_commit"  // a git-push redeploy via the HMAC webhook (Render's spelling)
	// TriggerConfigChange is a rollout a configuration write forced rather than
	// an explicit deploy verb: a Settings-page field, an env var or secret file,
	// an env-group link/unlink. bex cannot apply any of these without rolling a
	// new release, so each is a real deploy the user must be able to see, retry,
	// and roll back — spelled distinctly so the history still tells a settings
	// save apart from a manual deploy (w6/m51).
	TriggerConfigChange = "config_change"
)

The deploy `trigger` vocabulary — what caused a rollout. One place, since both the writers (CreateApp, deploys.Trigger/Rollback) and the readers (the events feed's deploy_started trigger flags, internal/events) spell it.

View Source
const (
	UsageKindInstanceSeconds  = "instance_seconds"
	UsageKindEgressBytes      = "egress_bytes"
	UsageKindBuildSeconds     = "build_seconds"
	UsageKindStorageGBSeconds = "storage_gb_seconds"
	// UsageKindSandboxComputeSeconds is measured in milli-vCPU-equivalent
	// seconds. Memory is folded into the weight at the AgentCore reference
	// ratio ($0.00945/GB-hour ÷ $0.0895/vCPU-hour); see ADR047 D6.
	UsageKindSandboxComputeSeconds = "sandbox_compute_seconds"
	// UsageKindDiskGBSeconds is a service disk's PROVISIONED capacity in GB
	// multiplied by the seconds it was provisioned for — deliberately a
	// different dimension from UsageKindStorageGBSeconds, which averages USED
	// bytes on a datastore volume. Disks bill on what was reserved, running or
	// not, because that is what Render charges for and what Hetzner charges
	// bex for; see docs/ADR082-persistent-disks.md D8/D9.
	UsageKindDiskGBSeconds = "disk_gb_seconds"
)

UsageKind names the metering dimensions. storage_gb_seconds is the average used datastore volume size in decimal GB multiplied by window seconds.

View Source
const (
	UsageSourceInstance  = "instance"
	UsageSourceBuild     = "build"
	UsageSourceStorage   = "storage"
	UsageSourceHTTP      = "http"
	UsageSourceWebSocket = "websocket"
	UsageSourceDirect    = "direct"
	UsageSourcePostgres  = "postgres"
	UsageSourceKeyValue  = "key_value"
	// UsageSourceSandbox is response-only for now: sandbox compute uses its
	// own durable lifecycle cursor rather than usage.Service's hourly source
	// observations, so any current-month sandbox total conservatively degrades
	// otherwise-known workspace coverage.
	UsageSourceSandbox = "sandbox"
	// UsageSourceDisk covers the provisioned-disk meter. Unlike every source
	// above it, it reads the control-plane's own disk lifecycle rows rather
	// than the cluster, so it is the one source that stays healthy while
	// Prometheus or the app cluster is unreachable.
	UsageSourceDisk = "disk"

	UsageSourceHealthy     = "healthy"
	UsageSourceDegraded    = "degraded"
	UsageSourceUnavailable = "unavailable"
)

Usage source-health vocabulary. Keep this closed and presentation-safe: the values are returned to clients when a current-month total is partial, so they must never contain provider errors, metric selectors, or tenant data.

View Source
const (
	ResourceKindService  = "service"   // App (web/worker/cron/static)
	ResourceKindPostgres = "postgres"  // Database CR → CNPG Cluster
	ResourceKindKeyValue = "key_value" // KeyValue CR → Valkey StatefulSet
	ResourceKindSandbox  = "sandbox"   // hosted OpenSandbox execution environment
)

ResourceKind identifies what type of resource a usage row belongs to. "service" is the default (App CRs); "postgres" and "key_value" are the managed-datastore kinds added in w8/m5.

View Source
const (
	WebhookAttemptPending   = "pending"
	WebhookAttemptDelivered = "delivered"
	WebhookAttemptFailed    = "failed"

	WebhookAttemptAutomatic = "automatic"
	WebhookAttemptManual    = "manual"
)
View Source
const (
	MaxActiveWebPushBrowsersPerSubject   = 10
	MaxActiveWebPushBrowsersPerWorkspace = 1000
)
View Source
const (
	WorkspaceCreationPrepared       = "prepared"
	WorkspaceCreationSetupPending   = "setup_pending"
	WorkspaceCreationSetupSucceeded = "setup_succeeded"
	WorkspaceCreationFinalized      = "finalized"
	WorkspaceCreationCleanupPending = "cleanup_pending"
	WorkspaceCreationExpired        = "expired"
)
View Source
const (
	MemberKindUser    = "user"
	MemberKindMachine = "machine"
)

The two kinds of tenant_members row. A machine row is what makes a bound API key resolve to its workspace and authorize there — it is not a member of the workspace in any sense the Team surface or the seat cap should mean.

View Source
const BlueprintRunInterruptedReason = "" /* 143-byte string literal not displayed */

BlueprintRunInterruptedReason is the stored error_message for an abandoned run: an actionable retry instruction, never an automatic replay.

View Source
const BlueprintRunRecoveryBound = 30 * time.Minute

BlueprintRunRecoveryBound is the documented staleness bound: a running sync with no terminal state older than this is abandoned as interrupted, and a disconnect may reap an active claim older than this inline. Healthy applies are expected to finish well inside it; a live run past the bound is settled as interrupted anyway (its late completion is then fenced, never applied as success), which bounds the busy window after process loss.

View Source
const DefaultFeedPark = time.Minute

DefaultFeedPark bounds how far the durable watermark may lag the read window before an otherwise-quiet pass persists it forward.

It lives here, like FeedCommitLag, because it is a property of the shared watermark row rather than a per-consumer tunable: parking per tick would be a Postgres write transaction every poll interval forever on a platform where nothing is happening, and parking never makes a restart re-read the whole empty window. A minute makes a quiet-but-subscribed platform cost one small write a minute and re-read at most a minute of already-empty window — the same trade for every tailer. Page size, by contrast, is genuinely per-consumer (the push worker does per-row lookups that the webhook worker does not) and stays in each package.

View Source
const DefaultPlan = PlanHobby

DefaultPlan is the plan a new workspace gets when the caller names none — the free tier, matching Render's create flow default.

View Source
const FeedCommitLag = 3 * time.Second

FeedCommitLag is how far behind now a tailer of ListWebhookEvents must read. The feed's timestamps are assigned before their rows commit (Go's clock for audit rows, the statement's now() for deploys), so a row can appear under an already-advanced watermark; reading only rows older than this leaves in-flight commits time to land. A transaction slower than this can still slip under — accepted, documented, and bounded (audit inserts time out at 2s, core/audit.go).

It lives with the query rather than with either consumer because it is a property of THIS feed's write path, not a per-consumer tunable: every tailer of ListWebhookEvents (internal/webhooks, internal/notifications) is exposed to the same in-flight commits and must use the same lag. Page size and park interval are genuinely per-consumer and stay in their own packages.

View Source
const FirstDeployGeneration int64 = 1

FirstDeployGeneration is the release generation a service's create-triggered deploy runs under: a freshly created Kubernetes object always starts at metadata.generation 1, and the reconciler projects this row into exactly such an object. bex-api stamps the same value on the CR's release-generation annotation, and the two MUST agree — a row pinned to a release the operator never reports reads as superseded and closes canceled (w6/m46 t004).

View Source
const MaxAgentSessionPromptBytes = 8 << 20

MaxAgentSessionPromptBytes caps all durable user intent for one session. Individual prompts are already limited to 100 KiB at the API and schema; this aggregate bound prevents an unbounded number of valid turns from growing Postgres independently of the assistant-transcript quota.

View Source
const MaxAgentSessionTranscriptBytes = 64 << 20

MaxAgentSessionTranscriptBytes caps the total payload bytes one session's transcript may hold (w1/m65 F10), bounding Postgres growth from tenant-controlled agent output and replay memory. Every write path (gateway live splice, gateway prompt turn, Completer harvest) seeds its byte counter from the already-stored total (AgentSessionTranscriptBytes) and stops appending at this cap; the replay read (AgentSessionTranscript) is budgeted by the same bound, so it never materializes more than the cap.

View Source
const MaxBillingNotificationAttempts = 24

MaxBillingNotificationAttempts caps how often a billing notice is claimed. With the worker's backoff (doubling to 128 minutes) the last attempt falls about 36 hours after the first. A notice whose last attempt fails stays undelivered with its last_error, due at infinity, so the claim scan never reaches it again (w5/m136).

View Source
const MaxReplicas = appv1alpha1.MaxReplicas

MaxReplicas is the shared upper bound on an App's replica count, enforced by both the create path (store/api.go) and the apps scale verb so the two can't disagree about what a valid App is. The lower bounds legitimately differ (create treats 0 as "default 1"; scale rejects 0 — see apps.Service.Scale). The value is the CRD/operator ceiling (types.MaxReplicas) so autoscaling, explicit scale, and create cannot drift from what Kubernetes will admit.

View Source
const MaxWebhookEndpointsPerWorkspace = 25

MaxWebhookEndpointsPerWorkspace bounds how many endpoints one workspace may register (w1/m67 F2). Every event is fanned out across every enabled endpoint of its workspace by a worker SHARED with every other tenant, so an unbounded endpoint count is an unbounded multiplier on shared memory and database work — growable persistently, by an ordinary workspace admin, at no cost to them. The value is far above any real integration topology (Render's own docs describe a handful of endpoints per workspace); it exists to stop the pathological case, not to shape normal use.

View Source
const MaxWebhookURLBytes = 2048

MaxWebhookURLBytes is the destination length cap (codex round-15 #2). The shared worker reloads every enabled endpoint every two seconds, so an unbounded HTTPS URL (the global 2 MiB body cap) is a persistent memory and query-byte multiplier across tenants. 2048 is far above any real webhook destination (Slack/PagerDuty/GitHub are hundreds of bytes) and matches the practical URL length most intermediaries accept.

Variables

View Source
var (
	ErrAgentSessionPromptQuota = fmt.Errorf("agent session prompt byte quota reached: %w", ErrConflict)
	ErrAgentSessionTurnState   = fmt.Errorf("agent session cannot accept another turn in its current state: %w", ErrConflict)
)
View Source
var (
	ErrPushDeviceSubjectLimit   = fmt.Errorf("member has the maximum number of active push devices: %w", ErrConflict)
	ErrPushDeviceWorkspaceLimit = fmt.Errorf("workspace has the maximum number of active push devices: %w", ErrConflict)
)
View Source
var (
	// ErrEnvGroupNameTaken is the (tenant_id, name) unique violation: another
	// group in the workspace already uses the name.
	ErrEnvGroupNameTaken = errors.New("environment group name taken")
	// ErrEnvGroupChanged is a lost compare-and-set: the row exists but its
	// version moved, or the operation it names is no longer the active one.
	ErrEnvGroupChanged = errors.New("environment group changed")
)
View Source
var (
	ErrOwnerMember = fmt.Errorf("%w: cannot remove the workspace owner", ErrConflict)
	ErrOwnerRole   = fmt.Errorf("%w: the workspace owner must remain an admin", ErrConflict)
)

ErrOwnerMember / ErrOwnerRole are the in-transaction backstops for the workspace owner binding (w5/m101), the same shape ErrLastAdmin has: the members service refuses these cases up front with a coded error, and the store rechecks under the membership advisory lock so a second adapter or a race cannot slip past the service gate.

Only the member verbs are guarded. RemoveAccountMember (ADR086 offboarding) deliberately removes an owner — it clears the binding in the same transaction, so the workspace never keeps a dangling owner.

View Source
var (
	ErrInviteAlreadyAccepted = fmt.Errorf("invite already accepted: %w", ErrConflict)
	ErrInviteExpired         = fmt.Errorf("invite expired: %w", ErrConflict)
	ErrInvitePlanLimit       = fmt.Errorf("workspace plan cannot seat invite: %w", ErrConflict)
)

Stable direct-invite redemption causes. Each wraps ErrConflict so existing REST/MCP status semantics remain unchanged while callers can classify the refusal without parsing human prose.

View Source
var (
	ErrNotFound = errors.New("not found")
	ErrConflict = errors.New("already exists")
	ErrInvalid  = errors.New("invalid")
	// ErrAccountDeletionPending is returned by membership writers after they
	// serialize with a newly committed account-deletion intent.
	ErrAccountDeletionPending = errors.New("account deletion pending")
	// ErrWorkspaceGone reports that a write lost a race with the workspace's own
	// deletion: the row it would have referenced is already gone. It is narrower
	// than ErrNotFound on purpose — callers acknowledge it as a no-op instead of
	// retrying, so only the writes that can genuinely lose that race may return
	// it. Today that is the Stripe lifecycle path, whose events are emitted by
	// the deletion itself (see verifyBillingProviderMapping).
	ErrWorkspaceGone = errors.New("workspace no longer exists")
)

Error taxonomy shared by the store and the API: the store classifies Postgres failures into these, the API maps them to status codes (ErrInvalid→400, ErrNotFound→404, ErrConflict→409).

View Source
var (
	ErrWebPushSubjectLimit   = fmt.Errorf("member has the maximum number of active web-push browsers: %w", ErrConflict)
	ErrWebPushWorkspaceLimit = fmt.Errorf("workspace has the maximum number of active web-push browsers: %w", ErrConflict)
)

AllTriggers is every Trigger* value — the set REST conformance proves maps onto Render's enum (w8/058). A guard test keeps it in step with the consts.

View Source
var ErrBlueprintResourceConflict = fmt.Errorf("blueprint resource claim conflict: %w", ErrConflict)

ErrBlueprintResourceConflict wraps ErrConflict when another Blueprint already owns the (tenant, kind, name) claim. Callers map it to BLUEPRINT_RESOURCE_CONFLICT.

View Source
var ErrBlueprintSyncBusy = fmt.Errorf("blueprint execution busy: %w", ErrConflict)

ErrBlueprintSyncBusy reports that a Blueprint lifecycle verb lost (or never held) execution authority: another apply owns the active claim, or the caller's generation was fenced by a newer admission or a disconnect. It wraps core.ErrConflict through MapError so every adapter answers 409; the apps layer re-codes it as BLUEPRINT_SYNC_BUSY. Callers retry after the recorded run settles, or start an explicit new sync.

View Source
var ErrFeedCursorUnloaded = errors.New("feed cursor read before Load")

ErrFeedCursorUnloaded is returned when a pass is asked to tail from a cursor whose durable watermark was never read. Failing here is the point: the zero cursor is the beginning of time, so the alternative is a silent replay of every event in the feed.

View Source
var ErrGitWebhookReplayCapacity = errors.New("git webhook replay capacity reached")

ErrGitWebhookReplayCapacity is returned before insertion when a tenant's exact durable replay-row budget is exhausted.

View Source
var ErrGitWebhookReplayEpochRetired = errors.New("git webhook replay signing epoch retired")

ErrGitWebhookReplayEpochRetired fails closed when a replica tries to accept a signing key whose claims were already safely purged after its lease expired.

View Source
var ErrLastAdmin = fmt.Errorf("%w: cannot remove or demote the last admin of a workspace", ErrInvalid)

ErrLastAdmin is the typed refusal when a demotion or removal would leave a workspace with zero administrators (codex round-16 #3).

View Source
var ErrWebhookAttemptPending = fmt.Errorf("webhook attempt is already pending: %w", ErrConflict)

ErrWebhookAttemptPending refuses a second, distinct resend while one attempt for the same logical notification is already reserved. It bounds concurrent replay fan-out without weakening idempotent repeats of the same request key.

View Source
var ErrWebhookEndpointDisabled = fmt.Errorf("webhook endpoint is disabled: %w", ErrConflict)

ErrWebhookEndpointDisabled refuses a manual resend while its destination is disabled. It wraps ErrConflict so every adapter keeps the shared conflict classification while exposing a stable feature-specific code.

View Source
var ErrWebhookEndpointLimit = fmt.Errorf("workspace has the maximum number of webhook endpoints: %w", ErrConflict)

ErrWebhookEndpointLimit is the typed refusal when a workspace is already at MaxWebhookEndpointsPerWorkspace. Wraps ErrConflict so existing REST/GraphQL/MCP error mapping treats it like the other quota refusals (cf. ErrInvitePlanLimit).

View Source
var ErrWebhookEndpointNotFound = fmt.Errorf("webhook endpoint not found: %w", ErrNotFound)

ErrWebhookEndpointNotFound distinguishes a missing owner-scoped endpoint from a missing source attempt while preserving the shared not-found class.

WorkspacePlans is the catalog in ladder order (cheapest first), the source of truth for validation and any plan picker.

Functions

func CRName

func CRName(tenant, app string) string

CRName is the projected CR's name, "<tenant-id>-<app>". Both parts are API-validated DNS labels of ≤30 chars, so the result always fits the 63-char object-name limit. Delegates to core.CRName — the apps feature's create path (w4/m19) computes the identical name for the same row, so whichever side (bex-api's direct Create or this reconciler's own fallback-create) gets there first, the other recognizes the CR by its LabelAppID rather than re-creating it under a different name.

func CanAddMember

func CanAddMember(plan string, currentMembers int) bool

CanAddMember reports whether a workspace on the given plan can take another member given its current count — the single-member guard keyed on plan (Hobby caps at 1, paid plans are unlimited). A pure function so w4/m12's invite verb consults the one rule without a second copy, and so it stays out of the service-layer authz sweep (it's a predicate, not a gated verb).

func CanTransitionDeploy

func CanTransitionDeploy(from, to string) bool

CanTransitionDeploy is the evidence-backed transition table. It permits forward phase skips because the backend samples Kubernetes state rather than receiving every operator edge, but rejects phase regressions and every mutation out of a terminal state. live -> deactivated is the sole exception: a newer live deploy atomically makes the prior live revision historical.

func CancelRelease

func CancelRelease(ctx context.Context, c client.Client, a *appv1alpha1.App, generation int64, buildNamespace string) error

CancelRelease ends the release a deploy row requested — the half of a cancel the row cannot express — for user Cancel and for a rollout a suspend interrupted alike (w5/m114), so a resumed service serves its prior live release instead of rolling one its history says was canceled.

The stamp lands first. The operator is level-triggered and only this annotation makes prepareAppReleaseDecision take its canceled branch and settle the App (settleCanceledRelease): the last served release, or PhaseCanceled when none ever served. Deleting a deterministic build Job or kpack Image without it would only see the next pass recreate it, and an image-backed release — which has no build to delete — would roll on (w6/m52, w6/m104). A newer deploy stamps a newer release generation and so supersedes the marker.

Only the stamp can fail it: once that lands the release is ended — the operator settles from it and rewinds the release generation — and a caller that failed then would strand its row open (releaseCanceledFor closes it on the next pass regardless). The build deletes are best effort: a build left running can no longer promote.

func CheckOwnership

func CheckOwnership(ctx context.Context, pool *pgxpool.Pool) error

CheckOwnership verifies that every table in the public schema is owned by the role currently connected (CURRENT_USER). Returns an error listing any mis-owned tables. Call this at startup after Migrate so a hand-run migration executed as a superuser (which creates tables owned by that superuser rather than the application role) is caught before the first request.

Incident (2026-07-12): tenant_invites was owned by postgres instead of bex, causing "permission denied for table tenant_invites" on every invite-redemption call. This check would have surfaced that at startup with a clear error.

func CronRunNameFromStartedKey

func CronRunNameFromStartedKey(key string) (string, bool)

CronRunNameFromStartedKey recovers the run (Job) name from a cron-start feed row's Key. Starts recorded before w4/259 stored no run id, and the source key is insert-once, so the feed derives their id from the key instead. Anything not exactly "fact:cron:<appID>:<job name>:started" yields no name: a malformed key never manufactures an association.

func DeployFailureStatus

func DeployFailureStatus(status string) bool

DeployFailureStatus reports whether status is a terminal failure. These can be reached by a forward skip straight from queued (the backend samples Kubernetes state; a build can fail entirely between two passes), so the transition's own clock is not evidence of when their work began.

func DeployStatusStampsDispatch

func DeployStatusStampsDispatch(status string) bool

DeployStatusStampsDispatch reports whether TransitionDeploy may stamp started_at from its own clock on reaching status: entering an in-progress status IS the dispatch moment being observed. Every terminal status is not: a failure arrived at by a skip collapsed a 68-second failed build into a one-microsecond duration (w6/m123), and a first observation already at live reported an image rollout whose container had been up for 13 seconds as a zero-second deploy that began when it finished (w4/m156). Terminal closes stamp only observed evidence (the operator's recorded build window), or honestly nothing.

func DeployStatusStartsExecution

func DeployStatusStartsExecution(status string) bool

DeployStatusStartsExecution reports whether reaching status means the deploy's work has actually begun. A queued deploy is waiting behind another build and has dispatched nothing; canceled/deactivated are exits that can be reached without ever executing. Shared with the build_started event fact so the timeline cannot disagree with the deploy row about when a build began (w6/035). Note it says nothing about WHEN the work began: a terminal failure reached by a phase skip executed at some earlier, unobserved time — see DeployStatusStampsDispatch for the stamping half (w6/m123).

func DeployStatuses

func DeployStatuses() []string

DeployStatuses returns a copy of the complete stored status vocabulary. Callers may use it to validate surface/filter parity without mutating the store's transition model.

func GitWebhookSigningEpoch

func GitWebhookSigningEpoch(keyClass, secret string) string

GitWebhookSigningEpoch derives a non-secret stable identifier for the exact HMAC key a replica accepts. The epoch changes on key rotation, which is the only point at which claims made under the old key can safely be discarded.

func ImageRepository

func ImageRepository(v string) (string, error)

ImageRepository returns the registry host + repository of an image reference with Docker Hub's short forms normalized (`nginx` and `docker.io/library/nginx` are one repository) and the tag/digest dropped. Render's imageUrl contract compares exactly this: a per-deploy image must keep the service's configured host, repository and image name.

func IsOpenDeployStatus

func IsOpenDeployStatus(status string) bool

IsOpenDeployStatus reports whether status may still advance or be canceled.

func IsTerminalDeployStatus

func IsTerminalDeployStatus(status string) bool

IsTerminalDeployStatus reports whether status is a known terminal state.

func ManagedAppID

func ManagedAppID(labels map[string]string) string

ManagedAppID returns the control-plane app id a projected App CR's labels carry, or "" when the labels don't prove bex-controlplane ownership (a hand-applied CR with no Postgres source row to key a fact/row on). One helper so every feature that reads the app id off a CR does it the same way.

func MapError

func MapError(err error) error

MapError translates this taxonomy into core's, so a feature service can return a store error straight out of a verb and have every surface map it to the right status. The store error stays in the message (%v, not %w) — the core sentinel is what callers match on.

Features whose mapping differs deliberately (workspaces folds Conflict into ErrBadRequest; registrycreds and webhooks return bare sentinels) keep their own — this is the common case, not a mandate.

func Migrate

func Migrate(uri string) error

Migrate applies all pending schema migrations to the database at uri (postgres:// or postgresql://). The schema ships embedded in the binary so a deploy always carries the schema it expects; a no-op when already current.

func NormalizePlan

func NormalizePlan(plan string) (string, error)

NormalizePlan validates a caller-supplied workspace plan, defaulting empty to Hobby. Unknown plans are ErrInvalid, listing the valid ones — the same shape as the internal API's tier validation.

func NormalizeResourceKind

func NormalizeResourceKind(kind string) string

NormalizeResourceKind preserves the pre-resource-kind behavior for callers that omit the field. Persisted rows always carry an explicit kind.

func OwnerWorkspaces

func OwnerWorkspaces(workspace string, err error) ([]string, error)

OwnerWorkspaces adapts a by-id routing read that finds one workspace to core.ResourceOwner's shape: a missing row is no owner, not an error.

func RedactRepoURL

func RedactRepoURL(v string) string

RedactRepoURL strips embedded userinfo from a repo URL before it is shown to viewers — the read-side companion to ValidRepo's create/update-time refusal (round-6 #13): rows stored before the rule (or written by a future code path that forgets it) must not hand a same-workspace viewer another member's token. A password-less ssh:// username is preserved (not a credential); anything unparseable is returned as-is (it cannot be cloned from either).

func RenderDeployStatus

func RenderDeployStatus(status string) string

RenderDeployStatus maps a TERMINAL bex deploy status onto Render's deployStatus enum (succeeded|failed|canceled) — one mapping next to the vocabulary it interprets, shared by the events feed's deploy_ended details and the webhook payload's data.status so the same deploy can never be reported differently by the two (the drift w2/m10's `canceled` addition showed is possible).

func ResourceDisplayNameKey

func ResourceDisplayNameKey(kind, id string) string

ResourceDisplayNameKey is the map key the usage presenter already uses to pair a kind with an id; different kinds may legally share an id.

func RoleAllowedOnPlan

func RoleAllowedOnPlan(plan, role string) bool

RoleAllowedOnPlan reports whether role is assignable on plan — the single predicate members.guardPlanRole (invite/change-role) and workspaces.ChangePlan's downgrade guard both consult, mirroring CanAddMember's shape below.

func SandboxNamespace

func SandboxNamespace(workspaceID string) string

SandboxNamespace is the sandbox-regime namespace name for a workspace id.

func TailFeed

func TailFeed[T any](ctx context.Context, cur *FeedCursor, pass FeedPass[T]) error

TailFeed advances cur through the feed, projecting and committing a page at a time until a page comes back short or empty. The cursor is advanced only after its page's Commit returns, so a failed write is re-read rather than skipped.

func ValidGitRef

func ValidGitRef(v string) bool

ValidGitRef reports whether v is an acceptable git branch/tag/ref for a build-from-git App (no shell metacharacters, no leading dash). Single-source for both create paths (w6/m6 t003).

func ValidGlob

func ValidGlob(v string) bool

ValidGlob reports whether v is a safe build-filter glob pattern (Render's Build Filters, docs/ADR018): a non-empty, ≤512-byte, repository-root-relative pattern with no control characters, no backslash, no traversal ("..") component, and no leading "/", that doublestar can compile (Render's dialect — *, **, ?, [class]). Exported so bex-api's SetBuildFilter and create enforce one rule for every buildFilter pattern; a malformed pattern is rejected at the API boundary so the webhook matcher never sees one it can't compile.

func ValidRepo

func ValidRepo(v string) bool

ValidRepo reports whether v is an acceptable git repo URL for a build-from-git App (https/ssh/git@, no whitespace or control chars, ≤2048 bytes). Exported so bex-api and the internal create API enforce one rule (w6/m6 t003).

URL userinfo is refused (codex-security round-6 #13): the repo string is stored on the App and echoed verbatim to every workspace viewer, so https://user:token@host/… (or a PAT in the username position) would turn a non-secret resource identifier into a credential leak. Credentials belong in the GitHub App integration / clone Secret, never the URL. The one exception is a password-less ssh:// username (ssh://git@host/…): the SSH user selects the account server-side and is not a bearer secret; the SCP form (git@host:…) has no password syntax at all.

A colon-less git@ value is refused (codex-security 2026-08 F5): git treats a URL with no scheme AND no colon as a LOCAL path, which selects the local transport — the one transport where a fetch-injected --upload-pack=<cmd> option executes <cmd> locally in the credential-holding clone container. The colon is what makes "git@host:path" an SCP remote; without it the value was never a remote URL at all.

func ValidRootDir

func ValidRootDir(v string) bool

ValidRootDir reports whether v is a safe build root directory: a relative path with no traversal ("..") or absolute components and no control characters, ≤512 bytes. Empty (the default — build the repo root) is valid. Exported so bex-api's SetRootDir and create enforce one rule (w6/m6 t003).

func ValidateImage

func ValidateImage(v string) error

ValidateImage checks an OCI image reference's grammar and registry policy. Short Docker Hub names and tag+digest references are accepted without rewriting the stored value. The kubelet pulls from the node network, outside pod egress policy: private/reserved IPs and untrusted registry hosts must stay blocked. Checking DNS here would not protect the kubelet's later resolution from rebinding.

func ValidatePushNotification

func ValidatePushNotification(notification PushNotification) error

ValidatePushNotification is the producer-side admission seam. Feed projections must be able to drop one malformed logical item without making the shared watermark retry that poison event forever.

func WorkspaceNamespace

func WorkspaceNamespace(workspaceID string) string

WorkspaceNamespace is the hosting namespace name for a workspace id. The workspace id (tea-<xid>) is already a DNS-safe label ≤63 chars (ADR020), so it is used verbatim as the namespace name — the k8s object IS the tenant. core.AppInOwnWorkspaceNamespace assumes this identity mapping (core cannot import store); a non-identity mapping here must change that predicate too.

Types

type API

type API struct {
	Store Store
	// Kick, when non-nil, nudges the reconciler after each successful write
	// so intent reaches the cluster immediately instead of on the next resync.
	Kick func()
	// Health reports readiness (the DB ping) for /healthz.
	Health func(context.Context) error
	// Token gates /v1/ behind a constant-time bearer check. Empty leaves the
	// API open, so cmd/api refuses to start the :8091 listener with an empty
	// token unless BEX_CP_INSECURE=1 (w1/m53 requireCPAuth) — this field is
	// only ever empty in that explicit local-dev override.
	Token string
	// Grant, when set, writes the new tenant's OpenFGA workspace membership so
	// its owner can authorize resources (replacing the model's workspace:default
	// placeholder). Nil => the tenant row is still created, without a membership.
	Grant             MembershipGranter
	Billing           BillingAdmin
	BillingOperations BillingOperations
	// SandboxTenants resolves an OpenSandbox per-workspace key to its workspace
	// id for the GET /v1/sandbox-tenants tenant-lookup endpoint (w3/m32 t006).
	// Nil => the endpoint returns 503 (sandbox multi-tenancy not configured).
	SandboxTenants SandboxTenantResolver
}

API is the minimal product surface over the source of truth: create a tenant, create an app, add a domain. Validation (business logic) lives here — not in the operator, not in Postgres procedures. Writes land as rows; the Reconciler projects them into App CRs.

func (*API) Handler

func (a *API) Handler() http.Handler

Handler returns the wired mux:

GET  /healthz                    (open)     DB ping — 200/503
POST /v1/tenants  GET /v1/tenants
POST /v1/apps     GET /v1/apps   GET|DELETE /v1/apps/{id}
POST /v1/domains

type AbandonedBlueprintSync

type AbandonedBlueprintSync struct {
	RunID         string
	BlueprintID   string
	Generation    int64
	StartedAt     time.Time
	Owned         bool // blueprint still names this run as its active claim
	BlueprintGone bool // no live blueprint row to project onto (defensive)
}

AbandonedBlueprintSync is one stale running run the recovery sweep returns.

type AccountDeletion

type AccountDeletion struct {
	Subject       string
	DeletedMarker string
	State         string
	Workspaces    []AccountWorkspaceDisposition
	Attempts      int
	ClaimedUntil  *time.Time
	NextAttemptAt time.Time
	LastError     string
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

type AccountDeletionBlockedError

type AccountDeletionBlockedError struct {
	Workspaces []AccountWorkspaceDisposition
}

func (*AccountDeletionBlockedError) Error

type AccountWorkspaceDisposition

type AccountWorkspaceDisposition struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Action string `json:"action"`
}

type ActivePushSubscription

type ActivePushSubscription struct {
	TenantID   string
	Subject    string
	Role       string
	DeviceID   string
	Provider   string
	Platform   string
	Token      string          `json:"-"`
	P256dh     string          `json:"-"`
	Auth       string          `json:"-"`
	PushPolicy json.RawMessage `json:"-"`
	CreatedAt  time.Time
}

ActivePushSubscription is the internal-only delivery projection. Token is never selected by caller-facing device reads and can never enter JSON.

type AgentDispatch

type AgentDispatch struct {
	SessionDeleted    bool
	PreviousSandboxID string
	SessionID         string
	Turn              int
	WorkspaceID       string
	Legacy            bool
}

AgentDispatch survives the process that accepted a turn, including after abandonment. Tombstones are revisited to catch a late remote create response.

type AgentSession

type AgentSession struct {
	ID            string
	WorkspaceID   string
	Repo          string
	Branch        string
	AgentConfig   json.RawMessage
	SandboxID     string
	Phase         string
	Status        string
	HeadSHA       string
	PRURL         string
	PRNumber      int
	Evidence      json.RawMessage
	Turns         int
	DeliveryMode  string
	FailureReason string
	// FailureReasonCode is a stable code for the failure's cause when a client
	// acts on it, such as SANDBOX_CAPACITY_LIMIT; empty otherwise (w5/m132). Only
	// AbandonAgentDispatch writes one; every other failure write clears it.
	FailureReasonCode string
	CreatedAt         time.Time
	UpdatedAt         time.Time
	CanceledAt        *time.Time
	// Hibernation (ADR059 D2/D3, w2/m68). Pinned removes the retention delete
	// edge; SnapshotRef is the object-storage key (empty ⇒ no durable snapshot);
	// SnapshotBytes is the storage-metering + quota dimension; HibernatedAt/
	// RetainUntil drive the retention sweep (NULL while Active).
	Pinned        bool
	SnapshotRef   string
	SnapshotBytes int64
	SnapshotSHA   string
	HibernatedAt  *time.Time
	RetainUntil   *time.Time
	// ArchivedAt is the ADR065 D1 archive flag: LIST-state, orthogonal to phase.
	// NULL ⇒ the session is in the working set; set ⇒ it is archived (hidden from
	// the default list, mutation verbs refused) but still viewable.
	ArchivedAt *time.Time
	// InitialPrompt is create-only input used to atomically insert turn 1 with
	// the session row. It is not an agent_sessions column and is never scanned.
	InitialPrompt string
}

AgentSession is the durable control-plane record for one cloud coding-agent task (ADR047 D3). AgentConfig is kept as JSON because the driver-specific knobs evolve independently of the lifecycle contract; callers validate its public shape before persistence.

type AgentSessionFailure

type AgentSessionFailure struct {
	Reason string
	Code   string
}

AgentSessionFailure is why a dispatch failed: a sentence for the reader and, when a client acts on the cause, a stable code (w5/m132).

type AgentSessionListQuery

type AgentSessionListQuery struct {
	Archived      ArchivedFilter
	Phases        []string
	Repo          string
	CreatedBefore time.Time
	CreatedAfter  time.Time
	Cursor        string
	Limit         int
}

AgentSessionListQuery is the ADR065 D3 filter + keyset-page contract for the workspace session list. Cursor is the id of the prior page's last item (Render's cursor idiom); an unknown or foreign cursor yields an empty page. Limit must be normalized (>0) by the caller — the store never substitutes a default, so an unbounded read is impossible by construction.

type AgentSessionTranscriptPart

type AgentSessionTranscriptPart struct {
	Seq       int64
	PartIndex int64
	Turn      int
	Part      json.RawMessage
	CreatedAt time.Time
}

AgentSessionTranscriptPart is one durable UI-message-stream part (ADR047 D9, w3/m43, amended by w5/m71). Seq is the store-allocated monotonic replay cursor; PartIndex is the driver's turn-local emission ordinal. Part is the verbatim `data:` payload forwarded to a Vercel AI SDK client.

type AgentSessionTurn

type AgentSessionTurn struct {
	SessionID           string
	Turn                int
	Prompt              string
	DeliveryMode        string
	TranscriptComplete  bool
	TranscriptTruncated bool
	TruncationReason    string
	CreatedAt           time.Time
	StartedAt           *time.Time
	CompletedAt         *time.Time
}

AgentSessionTurn is the durable user intent for one accepted agent turn. Assistant output stays in AgentSessionTranscriptPart so the gateway can replay its exact wire bytes; this row supplies the role and completeness facts the byte stream itself cannot encode.

type App

type App struct {
	ID       string `json:"id"`
	TenantID string `json:"tenantId"`
	Name     string `json:"name"`
	// Slug is the globally-unique public subdomain (Render's "slug", distinct
	// from Name which is only workspace-unique): the bare Name when free
	// platform-wide, or "<name>-<4-char suffix>" when CreateApp had to mint one
	// to avoid a cross-tenant collision (w4/m19). The operator reads this —
	// never Name — to derive the platform host.
	Slug string `json:"slug"`
	// Type is the App's service kind (types.AppSpec.Type's vocabulary:
	// web_service | private_service | background_worker | cron_job |
	// static_site). The projector derives spec.expose from it, so a row that
	// does not carry it cannot be projected correctly. Immutable for the app's
	// whole life, so it is recorded once at create. Empty is a pre-w6/m46 row
	// and reads as web_service, exactly as the CRD contract defines the empty
	// type; the projector backfills the real value from the live CR on resync.
	Type  string `json:"type,omitempty"`
	Repo  string `json:"repo,omitempty"`
	Image string `json:"image,omitempty"`
	// RegistryCredentialID preserves Render's tri-state image credential
	// binding: nil is legacy host auto-resolution, pointer-to-empty explicitly
	// selects no credential, and a non-empty value pins one workspace credential.
	RegistryCredentialID *string   `json:"registryCredentialId,omitempty"`
	Branch               string    `json:"branch"`
	Port                 int32     `json:"port"`
	ContainerPolicy      string    `json:"containerPolicy,omitempty"`
	PortMode             string    `json:"portMode,omitempty"`
	Replicas             int32     `json:"replicas"`
	Tier                 string    `json:"tier"`
	IdleTTLSeconds       int32     `json:"idleTTLSeconds"`
	Suspended            bool      `json:"suspended"`
	ProjectID            string    `json:"projectId,omitempty"`
	EnvironmentID        string    `json:"environmentId,omitempty"`
	CreatedAt            time.Time `json:"createdAt"`
	// DisplayName is the read projection of the CR's mutable spec.displayName
	// (SetAppDisplayName). Empty means never renamed; readers fall back to
	// Name. Read-only here: CreateApp does not write it.
	DisplayName string `json:"displayName,omitempty"`
	// CreationPending prevents the projector from dispatching a Blueprint App
	// before its initial environment-group composition has committed.
	CreationPending bool `json:"-"`
	// InitialDisk is CreateApp input only. Save the disk and its first metering
	// period in the service transaction, before a projector can observe the row.
	InitialDisk *appv1alpha1.DiskSpec `json:"-"`
	// FirstDeployCommit is CreateApp INPUT only (w9/001): the resolved commit
	// stamped onto the first deploy row (trigger "create") CreateApp opens in
	// the same transaction. Not an apps column — never persisted on, or read
	// back from, the app row itself. Zero value ⇒ the first deploy carries no
	// commit metadata (image-backed app, or no GitHub connection to resolve
	// the branch through).
	FirstDeployCommit CommitInfo `json:"-"`
	// FirstDeployID is CreateApp OUTPUT only (w3/m14): the id minted for the
	// first deploy row. Callers use it to navigate straight to the deploy page.
	// Not stored on the app row — only populated by CreateApp, never read back.
	FirstDeployID string `json:"-"`
}

App is a row of `apps` — the source-of-truth service definition the reconciler projects into an App CR. Observed state (phase, url) is NOT stored here: it lives on the App CR's status, which bex-api reads at query time.

type AppPlacement

type AppPlacement struct {
	TenantID      string
	ProjectID     string
	EnvironmentID string
}

AppPlacement is authoritative membership; runtime state remains on the App CR.

type ArchivedFilter

type ArchivedFilter int

Archived-membership selector for ListAgentSessions (ADR065 D3). The zero value excludes archived rows — the default working set.

const (
	ArchivedExclude ArchivedFilter = iota // archived_at IS NULL (default)
	ArchivedOnly                          // archived_at IS NOT NULL
	ArchivedInclude                       // no predicate — the full history
)

type AuditFilter

type AuditFilter struct {
	Since       time.Time
	Until       time.Time
	Cursor      string
	Limit       int
	OldestFirst bool
}

AuditFilter narrows ListAuditEvents: Since/Until bound At inclusively (Render's startTime/endTime); Cursor resumes strictly after a previously returned row's id (keyset paging, stable under concurrent inserts — unlike an OFFSET); Limit caps the page (<1 or >MaxAuditPageSize clamps to DefaultAuditPageSize). OldestFirst flips the order (Render's direction=forward, w4/013) — note a cursor's meaning follows the direction it is paged under: the same row id resumes strictly older rows backward and strictly newer rows forward.

type AuditRow

type AuditRow struct {
	ID           string
	WorkspaceID  string
	Caller       string
	CallerMethod string
	Verb         string
	Resource     string
	// Target is the resource the verb acted ON ("service:my-api",
	// core.ServiceTarget) — empty for a workspace-wide verb. Resource is what the
	// verb was authorized AGAINST (the workspace); Target is what it changed.
	Target string
	// TargetName is the resource's display name when a typed datastore effect
	// needs it for Render's thin webhook payload.
	TargetName string
	Outcome    string
	At         time.Time
	// MaintenanceModeTo is Render's MaintenanceModeEnabledEvent metadata.to.
	// It is nil for every other audit verb.
	MaintenanceModeTo *bool
	// RoleFrom/RoleTo are the team-membership verbs' typed role detail
	// (w1/m33, migration 0040): ChangeRole records old→new, Invite/AcceptInvite
	// record RoleTo alone. Nil for every other verb and for pre-0040 rows.
	// RevokedKeyCount is how many API keys a membership removal revoked
	// (w2/m163). Nil for every other verb; 0 means the member had none.
	RevokedKeyCount *int32
	RoleFrom        *string
	RoleTo          *string
	// Relation is the RelCan… the decision was made against. Empty on typed
	// system events and pre-0088 rows.
	Relation string
	// OAuthClientID / OAuthAudience / OAuthScopes are the verified grant
	// facts for a human OAuth caller. Empty on session, machine, system, and
	// pre-0088 rows.
	OAuthClientID string
	OAuthAudience string
	OAuthScopes   []string
}

AuditRow is one persisted audit_events row.

type AutoDeployFilter

type AutoDeployFilter int16

AutoDeployFilter constrains the auto_deploy_enabled column of apps.SetAutoDeploy rows on the audit arm. It is used when the caller filters by an auto-deploy event type so that the discrimination (enabled vs disabled vs legacy changed) runs in SQL before the LIMIT rather than in Go after it. A Go-side drop after LIMIT would return short (or empty) pages, which a cursor client reads as end-of-feed.

It is a bit set, not an enum (w1/m165): Render's type filter takes several types at once (`type=autodeploy_enabled,autodeploy_disabled`), so the allowed sub-types are OR-ed together. It constrains ONLY SetAutoDeploy rows — every other verb in the same multi-type request passes through untouched.

const (
	// AutoDeployFilterNone imposes no constraint — all auto_deploy_enabled values pass.
	AutoDeployFilterNone AutoDeployFilter = 0
	// AutoDeployFilterEnabled selects rows where auto_deploy_enabled = true.
	AutoDeployFilterEnabled AutoDeployFilter = 1
	// AutoDeployFilterDisabled selects rows where auto_deploy_enabled = false.
	AutoDeployFilterDisabled AutoDeployFilter = 2
	// AutoDeployFilterChanged selects rows where auto_deploy_enabled IS NULL (legacy rows).
	AutoDeployFilterChanged AutoDeployFilter = 4
)

type BillingAdmin

type BillingAdmin interface {
	OverrideBilling(context.Context, string, string, string, string, time.Duration) (BillingLifecycle, error)
}

type BillingEnforcement

type BillingEnforcement struct {
	WorkspaceID  string
	ResourceKind string
	ResourceName string
	MarkerID     string
	EnforcedAt   time.Time
	RecoveredAt  *time.Time
}

type BillingExportIssue

type BillingExportIssue struct {
	TransactionID string     `json:"transactionId"`
	WorkspaceID   string     `json:"workspaceId"`
	ResourceKind  string     `json:"resourceKind"`
	ServiceID     string     `json:"serviceId"`
	Kind          string     `json:"kind"`
	Tier          string     `json:"tier,omitempty"`
	WindowStart   time.Time  `json:"windowStart"`
	EventName     string     `json:"eventName"`
	IssueKind     string     `json:"issueKind"`
	ErrorCode     string     `json:"errorCode"`
	ErrorMessage  string     `json:"errorMessage"`
	FirstSeenAt   time.Time  `json:"firstSeenAt"`
	LastSeenAt    time.Time  `json:"lastSeenAt"`
	ResolvedAt    *time.Time `json:"resolvedAt,omitempty"`
	Resolution    string     `json:"resolution,omitempty"`
	Actor         string     `json:"actor,omitempty"`
	Reason        string     `json:"reason,omitempty"`
}

type BillingExportReport

type BillingExportReport struct {
	WorkspaceID    string             `json:"workspaceId"`
	CustomerID     string             `json:"customerId,omitempty"`
	SubscriptionID string             `json:"subscriptionId,omitempty"`
	Livemode       *bool              `json:"livemode,omitempty"`
	Rows           []BillingExportRow `json:"rows"`
}

type BillingExportRow

type BillingExportRow struct {
	WorkspaceID   string     `json:"workspaceId"`
	ResourceKind  string     `json:"resourceKind"`
	ServiceID     string     `json:"serviceId"`
	Kind          string     `json:"kind"`
	Tier          string     `json:"tier,omitempty"`
	WindowStart   time.Time  `json:"windowStart"`
	Quantity      int64      `json:"quantity"`
	State         string     `json:"state"`
	AttemptedAt   *time.Time `json:"attemptedAt,omitempty"`
	EmittedAt     *time.Time `json:"emittedAt,omitempty"`
	TransactionID string     `json:"transactionId,omitempty"`
	EventName     string     `json:"eventName,omitempty"`
	ErrorCode     string     `json:"errorCode,omitempty"`
}

type BillingExportStats

type BillingExportStats struct {
	PendingRows        int64
	OldestPendingAge   time.Duration
	RejectedRows       int64
	AmbiguousRows      int64
	WithheldWorkspaces int64
}

BillingExportStats is the low-cardinality snapshot exported to Prometheus.

type BillingLifecycle

type BillingLifecycle struct {
	WorkspaceID        string     `json:"workspaceId"`
	Status             string     `json:"status"`
	Reason             string     `json:"reason,omitempty"`
	GraceDeadline      *time.Time `json:"-"`
	SourceEventID      string     `json:"-"`
	SourceEventAt      *time.Time `json:"-"`
	SourceEventOutcome string     `json:"-"`
	InvoiceID          string     `json:"-"`
	SubscriptionID     string     `json:"-"`
	TransitionVersion  int64      `json:"-"`
	RetryAt            *time.Time `json:"-"`
	ClaimedUntil       *time.Time `json:"-"`
	AttemptCount       int        `json:"-"`
	LastError          string     `json:"-"`
	EnforcedAt         *time.Time `json:"-"`
	RecoveredAt        *time.Time `json:"-"`
	RecoveryTarget     string     `json:"-"`
	CreatedAt          time.Time  `json:"-"`
	UpdatedAt          time.Time  `json:"updatedAt"`
}

BillingLifecycle is the provider-neutral durable dunning state.

type BillingNotification

type BillingNotification struct {
	WorkspaceID       string
	TransitionVersion int64
	Status            string
	Reason            string
	GraceDeadline     *time.Time
	Livemode          bool
	AttemptCount      int
	// MailedTo lists the owner subjects this notice already reached, so a
	// retry mails only the rest (w5/m136).
	MailedTo []string
}

type BillingOperations

type BillingOperations interface {
	BillingExportReport(context.Context, string, time.Time, time.Time) (BillingExportReport, error)
	ListBillingExportIssues(context.Context, bool, int) ([]BillingExportIssue, error)
	ResolveBillingExportIssue(context.Context, string, string, string, string, time.Time) (BillingExportIssue, error)
}

type BillingOverrideRequest

type BillingOverrideRequest struct {
	Action    string `json:"action"`
	Actor     string `json:"actor"`
	Reason    string `json:"reason"`
	Extension string `json:"extension,omitempty"`
}

type BillingProviderMapping

type BillingProviderMapping struct {
	WorkspaceID    string
	CustomerID     string
	SubscriptionID string
	Livemode       bool
	UpdatedAt      time.Time
}

BillingProviderMapping is the durable relationship that lets a signed Stripe event be resolved without a network call on the webhook request path. Provider ids never enter tenant-editable state or public API results.

type Blueprint

type Blueprint struct {
	ID         string     `json:"id"`
	TenantID   string     `json:"tenantId"`
	Name       string     `json:"name"`
	Repo       string     `json:"repo"`
	Branch     string     `json:"branch"`
	Path       string     `json:"path"`
	AutoSync   bool       `json:"autoSync"`
	Manifest   string     `json:"manifest"`
	Status     string     `json:"status"`
	LastSyncAt *time.Time `json:"lastSyncAt"`
	CreatedAt  time.Time  `json:"createdAt"`
	UpdatedAt  time.Time  `json:"updatedAt"`
	// ExecutionGeneration is the lifecycle fencing counter (w8/m37, migration
	// 0111): bumped by every admission, disconnect, and explicit
	// re-creation. Only the Admit/Complete paths populate it; the remaining
	// readers leave it zero rather than pay for columns they never use.
	ExecutionGeneration int64 `json:"-"`
	// ActiveRunID names the one admitted-but-uncompleted run, "" when idle.
	// Internal to lifecycle coordination; never rendered on API views.
	ActiveRunID string `json:"-"`
}

Blueprint is a row of `blueprints` — a workspace-scoped render.yaml stack source. Created automatically when deploy is called with a repo+manifest; sync re-applies the stored manifest (optionally replacing it first). w2/m62: path/AutoSync/LastSyncAt added for Git-connected instance semantics.

type BlueprintAutoSyncIntent

type BlueprintAutoSyncIntent struct {
	ID             string     `json:"id"`
	TenantID       string     `json:"tenantId"`
	BlueprintID    string     `json:"blueprintId"`
	DeliveryDigest string     `json:"deliveryDigest"`
	CommitSHA      string     `json:"commitSha"`
	Path           string     `json:"path"`
	State          string     `json:"state"`
	ErrorMessage   *string    `json:"errorMessage,omitempty"`
	CreatedAt      time.Time  `json:"createdAt"`
	ClaimedAt      *time.Time `json:"claimedAt,omitempty"`
	CompletedAt    *time.Time `json:"completedAt,omitempty"`
}

BlueprintAutoSyncIntent is a durable accepted automatic Blueprint sync (w8/m38): persisted before the git-webhook acknowledges the delivery so a process restart cannot lose work the HMAC already authorized.

type BlueprintClaimKind

type BlueprintClaimKind string

BlueprintClaimKind is the kind of resource a claim names. The constants are exactly the set blueprint_resource_claims_kind_chk admits (w5/115).

const (
	BlueprintClaimService  BlueprintClaimKind = "service"
	BlueprintClaimDatabase BlueprintClaimKind = "database"
	BlueprintClaimKeyValue BlueprintClaimKind = "key_value"
)

func BlueprintClaimKinds

func BlueprintClaimKinds() []BlueprintClaimKind

BlueprintClaimKinds is every claim kind. Tests range over it, so a new kind cannot ship without its CHECK migration or its manifest kind.

type BlueprintResourceClaim

type BlueprintResourceClaim struct {
	TenantID    string
	Kind        BlueprintClaimKind
	Name        string
	BlueprintID string
	ClaimedAt   time.Time
}

BlueprintResourceClaim is the durable owner of one workspace resource name (w8/m40).

func (BlueprintResourceClaim) Key

Key names the resource the claim is on.

type BlueprintResourceKey

type BlueprintResourceKey struct {
	Kind BlueprintClaimKind
	Name string
}

BlueprintResourceKey names one claimable resource in a workspace.

type BlueprintSync

type BlueprintSync struct {
	ID           string     `json:"id"`
	BlueprintID  string     `json:"blueprintId"`
	CommitID     string     `json:"commitId"`
	State        string     `json:"state"`
	StartedAt    time.Time  `json:"startedAt"`
	CompletedAt  *time.Time `json:"completedAt"`
	CreatedAt    time.Time  `json:"createdAt"`
	ErrorMessage *string    `json:"errorMessage"`
	// Note annotates what the run did when its state cannot say it — today,
	// that a confirmed takeover replaced a named blueprint on the same
	// (tenant, repo, branch) row (w4/m125, migration 0131). Empty means
	// nothing to report, the same convention deploys.stall_reason uses.
	Note string `json:"note,omitempty"`
	// ExecutionGeneration records which admission a run belongs to (w8/m37,
	// migration 0111) so recovery and completion never settle a run against a
	// newer generation. Internal; never rendered on API views.
	ExecutionGeneration int64 `json:"-"`
}

BlueprintSync is a row of `blueprint_syncs` — one recorded sync run. State machine: created → running → success | error.

type CLITelemetryEvent

type CLITelemetryEvent struct {
	ID                    string
	Subject               string
	WorkspaceID           string
	ReportedWorkspaceID   string
	Command               string
	DurationMs            int64
	ExitCode              int
	CompletionKind        string
	CLIVersion            string
	OS                    string
	Arch                  string
	OutputFormat          string
	InstallationID        string
	BexVersion            string
	LaunchedFullScreenTUI *bool
	IsStdinTTY            bool
	IsStdoutTTY           bool
	IsStderrTTY           bool
	IsTermDumb            bool
	AgentSignals          string
	CISignals             string
	StartedAt             *time.Time
	ReceivedAt            time.Time
}

CLITelemetryEvent is one ingested `POST /v1/cli-telemetry-events` delivery (w5/m92). Subject/WorkspaceID are the reporter's auth attribution, resolved server-side and never taken from the client's claim; ReportedWorkspaceID keeps the CLI's own current_workspace_id for join/debug. The remaining fields mirror Render's CliTelemetryEventPOSTInput JSON shape one-for-one so the imported CLI's sender needs no translation. BexVersion is the exception: it is bex's own launcher release, carried by a header rather than the body precisely so that shape stays diffable against upstream (w5/m94), and is empty for an unmodified upstream CLI or any pre-m94 build.

type CloneSecreter

type CloneSecreter interface {
	EnsureCloneSecret(ctx context.Context, namespace, appName, workspaceID, repo string) (string, error)
}

CloneSecreter is called by the Reconciler when projecting a new App CR for a repo-backed row: it mints a GitHub installation token, writes the <app>-clone Secret, and returns the Secret name for spec.cloneSecret — so private-repo builds from the first projected deploy authenticate without a separate API call. apps.Service satisfies this; cmd/api injects it when both the GitHub App and the control-plane store are wired. nil => public-clone only (no authentication, unchanged behaviour).

type CommitInfo

type CommitInfo struct {
	Hash     string     `json:"hash,omitempty"`
	Message  string     `json:"message,omitempty"`
	AuthorAt *time.Time `json:"authorAt,omitempty"`
}

CommitInfo is the git commit a build-from-git deploy runs — the resolved SHA, message, and author timestamp (w9/001 + w2/m42). Zero value = unknown: image-backed app, no GitHub connection to resolve through, resolution failed, or no prior deploy ever recorded a commit. Callers that pass zero mean "none" — CreateDeploy does not inherit. The rollout tracker is what carries a prior release's commit forward onto config_change re-rolls (hash + message + authorAt); image-backed apps with no prior commit stay empty. Commit metadata is best-effort provenance, never load-bearing — a deploy with an empty CommitInfo is still a fully valid deploy.

type CreateAppRequest

type CreateAppRequest struct {
	TenantID       string `json:"tenantId"`
	Name           string `json:"name"`
	Repo           string `json:"repo"`
	Image          string `json:"image"`
	Branch         string `json:"branch"`
	Port           int32  `json:"port"`
	Replicas       int32  `json:"replicas"`
	Tier           string `json:"tier"`
	IdleTTLSeconds int32  `json:"idleTTLSeconds"`
}

CreateAppRequest is the POST /v1/apps body. One of repo/image is required; zero values fall back to the platform defaults (branch main, port 3000, one replica, the tenant-independent "free" tier).

type CreateDomainRequest

type CreateDomainRequest struct {
	AppID   string `json:"appId"`
	Host    string `json:"host"`
	Primary bool   `json:"primary"`
}

CreateDomainRequest is the POST /v1/domains body. Primary makes the host the app's canonical URL (the CR's spec.host).

type CreateTenantRequest

type CreateTenantRequest struct {
	Name string `json:"name"`
	Plan string `json:"plan"`
	// Admin, when set, is the owner identity (Kratos identity id or Hydra
	// client id) granted admin on the new workspace in OpenFGA. Optional —
	// omit to create the tenant row without a membership.
	Admin string `json:"admin,omitempty"`
}

CreateTenantRequest is the POST /v1/tenants body.

type DatastoreEventFact

type DatastoreEventFact struct {
	SourceKey   string
	WorkspaceID string
	DatastoreID string
	Kind        string
	Type        DatastoreEventFactType
	At          time.Time
	ReasonCode  string
	BackupName  string
	BackupError string
	VersionFrom string
	VersionTo   string
	// Scheduled distinguishes a nightly backup from an on-demand one. nil
	// means the fact has no schedule dimension at all (every availability
	// fact), which is why it is a pointer rather than a plain bool.
	Scheduled *bool
}

DatastoreEventFact is one closed, non-secret managed-datastore event record. SourceKey is a stable producer identity used for idempotency; it is never exposed directly. The backup/version/schedule detail fields are empty on every availability fact — they belong to the backup, restore, and major- upgrade kinds that share this table.

type DatastoreEventFactType

type DatastoreEventFactType string

DatastoreEventFactType is the closed vocabulary of managed-datastore event facts — the Database/KeyValue sibling of ServiceEventFactType. The names come from eventvocab so the projector here, the retrievable event index, and the subscription picker cannot drift; migration 0107's fact_type CHECK is the persisted half of the same list.

const (
	DatastoreFactPostgresUnavailable DatastoreEventFactType = eventvocab.TypePostgresUnavailable
	DatastoreFactPostgresAvailable   DatastoreEventFactType = eventvocab.TypePostgresAvailable
	DatastoreFactKeyValueUnhealthy   DatastoreEventFactType = eventvocab.TypeKeyValueUnhealthy
	DatastoreFactKeyValueAvailable   DatastoreEventFactType = eventvocab.TypeKeyValueAvailable

	DatastoreFactPostgresBackupCompleted  DatastoreEventFactType = eventvocab.TypePostgresBackupCompleted
	DatastoreFactPostgresBackupFailed     DatastoreEventFactType = eventvocab.TypePostgresBackupFailed
	DatastoreFactPostgresRestoreSucceeded DatastoreEventFactType = eventvocab.TypePostgresRestoreSucceeded
	DatastoreFactPostgresRestoreFailed    DatastoreEventFactType = eventvocab.TypePostgresRestoreFailed
	DatastoreFactPostgresUpgradeStarted   DatastoreEventFactType = eventvocab.TypePostgresUpgradeStarted
	DatastoreFactPostgresUpgradeSucceeded DatastoreEventFactType = eventvocab.TypePostgresUpgradeSucceeded
	DatastoreFactPostgresUpgradeFailed    DatastoreEventFactType = eventvocab.TypePostgresUpgradeFailed
)

type Deploy

type Deploy struct {
	ID             string     `json:"id"`
	AppID          string     `json:"appId"`
	Trigger        string     `json:"trigger"`
	Image          string     `json:"image,omitempty"`
	ResolvedImage  string     `json:"-"`
	RollbackOf     string     `json:"rollbackOf,omitempty"`
	Generation     int64      `json:"-"`
	Commit         string     `json:"commit,omitempty"`
	CommitMessage  string     `json:"commitMessage,omitempty"`
	CommitAuthorAt *time.Time `json:"commitAuthorAt,omitempty"`
	Status         string     `json:"status"`
	// OverlapPending distinguishes the one latest-pending overlap slot from an
	// active release that may itself report Render status queued while waiting
	// for build capacity. It is store-internal and never exposed on a surface.
	OverlapPending bool       `json:"-"`
	CreatedAt      time.Time  `json:"createdAt"`
	UpdatedAt      time.Time  `json:"updatedAt"`
	StartedAt      *time.Time `json:"startedAt,omitempty"`
	FinishedAt     *time.Time `json:"finishedAt,omitempty"`
	// PreDeployStatus is the pre-deploy command's outcome for this deploy
	// (w1/m33): ” (no step) | 'running' | 'succeeded' | 'failed' | 'canceled'. The reconciler
	// projects it from the App CR's status.preDeploy so a migration failure is
	// distinguishable from a health-check failure (both close as update_failed).
	PreDeployStatus string `json:"preDeployStatus,omitempty"`
	// FailureReason is the human-actionable cause stamped when the reconciler
	// closes this deploy failed (w9/011) — the App CR's Ready-condition message
	// (crash loop with the $PORT hint, image-pull failure, build error) or a
	// synthesized health-gate-timeout line. Empty on non-failed deploys. A bex
	// extension beyond Render's deploy shape, like RollbackOf and
	// PreDeployStatus.
	FailureReason string `json:"failureReason,omitempty"`
	// CancelReason is the neutral cause of a canceled deploy when the cancel
	// was not user-initiated (w4/089): "Superseded by dep-…" when a newer
	// release replaced this row (w5/m114: on every path that cancels it), or
	// the suspend that ended its rollout (w4/m171). Empty for deploys.Cancel
	// (w6/m52) and for every non-canceled status. Distinct from FailureReason so the
	// dashboard can render it without text-destructive treatment.
	CancelReason string `json:"cancelReason,omitempty"`
	// StallReason is why an OPEN deploy is not progressing (w4/m112) — the
	// operator's own Ready-condition diagnosis, projected onto the row each
	// reconcile pass and cleared the moment the row goes terminal (from then
	// on FailureReason/CancelReason own the story). Empty means "nothing to
	// report": either the rollout is progressing normally or no diagnosis has
	// been reached yet. Unlike FailureReason this is a live observation, not a
	// verdict — it must never be read as "this deploy failed".
	StallReason string `json:"stallReason,omitempty"`
	// TriggeredBy is core.Identity.Subject for the caller that opened this row
	// (w4/072) — same form as audit_events.caller. Empty for unattributed
	// triggers (git push, deploy hook). Not a Render deploy field — kept off
	// the REST wire; the events feed projects it as Details.TriggeredByUser.
	TriggeredBy string `json:"-"`
}

Deploy is a row of `deploys` — one rollout attempt of an app, Render's deploy history (list_deploys/get_deploy). Trigger is "create" (the app's first deploy, opened by CreateApp), "api" (an explicit POST .../deploys), "deploy_hook" (the service's secret URL), "new_commit" (a git-push redeploy via the HMAC webhook), or "rollback" (w2/m10: a deploy created by Rollback, RollbackOf naming the source deploy it restores). Commit/CommitMessage (w9/001) are the resolved commit a build-from-git deploy ran, captured once at open time via the workspace's GitHub App connection — "" when unresolvable (omitted by the views, not faked). ResolvedImage is the image this deploy actually put into the cluster (backfilled by CloseDeploy once the deploy reaches live — "" until then, and forever for one that never does): the only field Rollback (w2/m10) trusts as a restore target, since Image alone is "" for a build-from-git deploy until the build resolves it. Generation is the App CR's metadata.generation this deploy runs under, captured once at open time — Cancel (w2/m10) derives its build-Job identity from this stored value rather than the App's current generation, which a later, unrelated spec write could have already moved past.

type DeployFilter

type DeployFilter struct {
	Statuses       []string
	CreatedBefore  time.Time
	CreatedAfter   time.Time
	UpdatedBefore  time.Time
	UpdatedAfter   time.Time
	FinishedBefore time.Time
	FinishedAfter  time.Time
	Cursor         string
	Limit          int
}

DeployFilter narrows ListDeploys using Render's status and exclusive created/updated/finished time bounds. Cursor resumes strictly after a previously returned deploy's id (newest-first keyset paging, stable under concurrent inserts); an unknown cursor yields an empty page. Limit is clamped here by clampPageLimit: <=0 (absent) and >core.MaxPageLimit both become core.MaxPageLimit — no query runs uncapped.

type DeployNotification

type DeployNotification struct {
	TenantID string
	AppName  string
	Status   string
	// DeployID and CommitMessage enrich the notification email (w7/m44): the
	// deploy id builds the "View Logs" deep link, the commit message names what
	// was deploying. Both empty-friendly — an image-backed deploy has no commit.
	DeployID      string
	CommitMessage string
	// CommitSHA and RepoURL build the deploy email's "View commit" link (the
	// repo's web commit URL). Both empty-friendly — an image-backed deploy has
	// no repo, and a repo build without a resolved SHA has no target.
	CommitSHA string
	RepoURL   string
	// FailureReason is the operator's actionable diagnosis for a failed deploy
	// (w7/m79) — the same string the deploy row and every API surface carry.
	// The failure email is the first thing most people read when a deploy
	// breaks, and it named the commit and linked the logs without ever saying
	// what went wrong. Empty for a succeeded deploy and for a failure the
	// operator could not diagnose.
	FailureReason string
	// NotifyOnFail is the legacy failure-only override. NotificationsToSend is
	// the authoritative Render policy when non-empty.
	NotifyOnFail        string
	NotificationsToSend string
}

DeployNotification is what DeployNotifier fans out — the closed deploy's identity and outcome, plus the App's per-service notification override (NotifyOnFail: w4/m21, docs/render-artifacts/notify-on-fail.md — "default"/"notify"/"ignore", or "" for an App created before the field existed, equivalent to "default"). A struct, not positional args, matching this codebase's convention for a cross-package "fan out an event" boundary (core.AuditEvent, webhooks' payload/DueWebhookDelivery) — so the next Render field lands as one more struct field, not a sixth positional arg.

type DeployNotifier

type DeployNotifier interface {
	NotifyDeploy(ctx context.Context, n DeployNotification)
}

DeployNotifier is called by the Reconciler when recordDeploy closes a deploy as succeeded or failed (w3/m9) — it fans the outcome out to the workspace's members by email, per each member's notification preferences. *notifications.Service satisfies it structurally (this package cannot import notifications: notifications imports store for NotificationsStore, so the dependency must run the other way, same shape as CloneSecreter). nil => no notifications (store-off / feature-off mode, byte-identical to before this milestone). Best-effort: implementations must not return an error — a flaky relay must never block reconciliation.

type DesiredApp

type DesiredApp struct {
	App
	// Disk is the service's live disk, nil when it has none. The projector
	// turns it into spec.disk; clearing it detaches the volume.
	Disk          *Disk
	TenantName    string
	PrimaryHost   string
	Hosts         []string
	HostRedirects map[string]string
	// EnvironmentIPAllowList is the member's environment inbound-IP layer
	// (w4/m28), projected from the environments row at read time (the
	// EnvironmentLayerCIDRs shape member CRs carry) so a projector-created CR
	// starts consistent with the environments fan-out. Update-path syncs stay
	// fan-out-owned: this field is deliberately NOT in applyOwnedSpec.
	EnvironmentIPAllowList []string
}

type DevicePushSubscription

type DevicePushSubscription struct {
	TenantID         string     `json:"tenantId"`
	Subject          string     `json:"subject"`
	DeviceID         string     `json:"deviceId"`
	SessionID        string     `json:"-"`
	Provider         string     `json:"provider"`
	Platform         string     `json:"platform"`
	Token            string     `json:"-"`
	TokenDigest      string     `json:"-"`
	PreferenceID     string     `json:"preferenceRef,omitempty"`
	RevokedAt        *time.Time `json:"-"`
	CreatedAt        time.Time  `json:"createdAt"`
	UpdatedAt        time.Time  `json:"updatedAt"`
	LastRegisteredAt time.Time  `json:"lastRegisteredAt"`
}

DevicePushSubscription is one opaque provider destination. Token is an internal delivery capability: it is deliberately excluded from JSON and never projected by the caller-facing notifications service.

type Disk

type Disk struct {
	ID        string     `json:"id"`
	TenantID  string     `json:"tenantId"`
	AppID     string     `json:"appId"`
	Name      string     `json:"name"`
	MountPath string     `json:"mountPath"`
	SizeGB    int32      `json:"sizeGB"`
	CreatedAt time.Time  `json:"createdAt"`
	UpdatedAt time.Time  `json:"updatedAt"`
	DeletedAt *time.Time `json:"deletedAt,omitempty"`
}

DesiredApp is an apps row joined with everything projection needs: the owning tenant's name (part of the CR name) and the app's domains, primary first. Disk is a persistent volume attached to one service (docs/ADR082-persistent-disks.md). Size is PROVISIONED capacity: it is what the tenant is billed for whether the service is running, suspended, or scaled to zero, exactly as Render bills its disks and as Hetzner bills the volume underneath.

type DiskUsageRow

type DiskUsageRow struct {
	TenantID  string
	AppID     string
	GBSeconds int64
}

DiskUsageRow is one service's provisioned disk GB-seconds within a metering window, attributed to the owning service (ADR082 D9 — the disk is a property of its service, so the meter keys on srv-, not dsk-).

type Domain

type Domain struct {
	ID                   string     `json:"id"`
	AppID                string     `json:"appId"`
	Host                 string     `json:"host"`
	Primary              bool       `json:"primary"`
	RedirectForName      string     `json:"redirectForName,omitempty"`
	ClaimState           string     `json:"claimState"`
	Challenge            string     `json:"challenge,omitempty"`
	ChallengeVersion     int64      `json:"challengeVersion"`
	VerificationAttempts int64      `json:"verificationAttempts"`
	LastVerificationAt   *time.Time `json:"lastVerificationAt,omitempty"`
	VerifiedAt           *time.Time `json:"verifiedAt,omitempty"`
	CreatedAt            time.Time  `json:"createdAt"`
}

Domain is a row of `domains` — a BYOD custom domain attached to an app. The primary domain becomes the App CR's spec.host, the rest spec.hosts. ClaimState is durable ownership admission; only verified rows are projected into App CRs. TLS/cert status remains observed from the cluster.

type DomainDeclaration

type DomainDeclaration struct {
	Host            string
	Primary         bool
	RedirectForName string
}

DomainDeclaration is one desired host plus projection metadata. Replacing a declaration set preserves an unchanged row's claim state and challenge; newly declared hosts enter pending.

type DuePushDelivery

type DuePushDelivery struct {
	PushNotification
	DeviceID  string
	SessionID string
	Provider  string
	Platform  string
	// MemberRole is the recipient's CURRENT workspace role, joined at claim
	// time ("" = membership gone) — the send-time input to the destination
	// eligibility re-check (ADR087, w6/m137), read in the same query so the
	// check cannot use stale authorization input (the
	// ListActivePushSubscriptions pattern). Populated by
	// ClaimDuePushDeliveries only; the receipts claim has no eligibility
	// decision to make.
	MemberRole       string
	Token            string `json:"-"`
	P256dh           string `json:"-"`
	Auth             string `json:"-"`
	TokenDigest      string `json:"-"`
	ClaimedUntil     time.Time
	AttemptCount     int
	AcceptedAt       *time.Time
	ReceiptDueAt     *time.Time
	ProviderTicketID string
}

DuePushDelivery is a leased per-device row joined to its current active token. The token remains internal and is excluded from JSON by construction.

type DueWebhookAttempt

type DueWebhookAttempt struct {
	WebhookAttempt
	URL                   string
	Secret                string
	TenantID              string
	EndpointName          string
	CreatedBy             string
	AutomaticAttemptCount int
}

DueWebhookAttempt is one pending attempt joined with its immutable parent request and endpoint delivery capability. AutomaticAttemptCount is the completed automatic-send count used by the worker's fixed retry budget; manual attempts never increment it.

type EnsureWatermark

type EnsureWatermark func(ctx context.Context, at time.Time) (time.Time, string, error)

EnsureWatermark seeds a tailer's durable watermark at `at` if none exists yet and returns the current one — EnsureWebhookWatermark / EnsurePushWatermark.

type EnvGroup

type EnvGroup struct {
	ID            string
	TenantID      string
	Name          string
	EnvironmentID string // storage id ("env-…"); "" for a workspace-scoped group
	ServiceLinks  []string
	EnvKeys       []string
	FileNames     []string
	CreatedAt     time.Time
	UpdatedAt     time.Time
	MetaVersion   int64
	Content       EnvGroupContent
}

EnvGroup is a row of env_groups (w8/m114): every non-secret fact about one environment group. Its env and files maps stay in OpenBao.

type EnvGroupCommit

type EnvGroupCommit struct {
	EnvKeys   []string
	FileNames []string
	UpdatedAt time.Time
}

EnvGroupCommit is what a content-changing save leaves on the row.

type EnvGroupContent

type EnvGroupContent struct {
	State      string
	Generation uint64
	Version    int64
	Op         *EnvGroupOp
}

EnvGroupContent is the coordination state of the group's content: which generation is committed, whether a save holds it, and that save's operation.

type EnvGroupOp

type EnvGroupOp struct {
	ID           string
	Kind         string
	Phase        string
	LeaseUntil   time.Time
	EnvChanged   bool
	FilesChanged bool
}

EnvGroupOp is the in-flight (or stranded) content operation recorded with the claim, so another API instance can recover it after its lease.

type EnvGroupRelease

type EnvGroupRelease struct {
	RepairRequired  bool
	RetainOperation bool // idle operation is durable evidence of pending value-map cleanup
	Commit          *EnvGroupCommit
}

EnvGroupRelease ends a claim. By default it releases the group idle and clears the operation, with the committed content unchanged. RepairRequired keeps the operation as the evidence recovery needs instead. RetainOperation leaves it on an idle row until recovery value-map cleanup succeeds. Commit records that the save changed content: the next generation, with these names.

type Environment

type Environment struct {
	ID                      string                  `json:"id"`
	ProjectID               string                  `json:"projectId"`
	TenantID                string                  `json:"tenantId"`
	Name                    string                  `json:"name"`
	CreatedAt               time.Time               `json:"createdAt"`
	ProtectedStatus         string                  `json:"protectedStatus"`
	NetworkIsolationEnabled bool                    `json:"networkIsolationEnabled"`
	IPAllowList             []core.IPAllowListEntry `json:"ipAllowList"`
}

Environment is a row of `environments` — a named subset of a Project's services (e.g. staging/production). Services opt-in by having environment_id set in `apps`, the same shape Project's project_id uses.

ProtectedStatus/NetworkIsolationEnabled/IPAllowList (0023, w6/m19) are Render's protected-environment ACLs: ProtectedStatus is "protected" or "unprotected" (never empty — the column CHECK + DEFAULT enforce it); NetworkIsolationEnabled, when true, is what makes environments/service.go stamp core.LabelEnvironment onto member App CRs for the operator's environment-scoped NetworkPolicy; IPAllowList is fanned out onto member Database/KeyValue CRs' own Spec.IPAllowList (environments/acl.go). Since w4/m24 (0034) made ip_allow_list a jsonb array; w1/m56 (0053) normalized its remaining rows to required {cidrBlock, description} entries before the wire/store decoder became strict.

type EventFactWriter

type EventFactWriter interface {
	InsertServiceEventFact(ctx context.Context, fact ServiceEventFact) (bool, error)
}

EventFactWriter is the narrow producer seam used by apps and webhook code.

type FeedCursor

type FeedCursor struct {
	// contains filtered or unexported fields
}

FeedCursor is one tailer's cached position in the composed feed: the durable watermark, read once per process and advanced in memory by each committed page. The cache is a pure optimization under multiple replicas — a stale cursor re-reads rows the consumer's own idempotency key then dedupes, so it converges rather than duplicating.

The zero value is UNLOADED and is not usable until Load runs.

func (*FeedCursor) Load

func (c *FeedCursor) Load(ctx context.Context, ensure EnsureWatermark, at time.Time) error

Load reads the durable watermark once per process, seeding it at `at` on first start so a newly enabled feature does not replay the feed's whole history. It is idempotent: once loaded, TailFeed's own commits keep the cache current, so later calls are a no-op and cost no round trip.

func (*FeedCursor) Loaded

func (c *FeedCursor) Loaded() bool

Loaded reports whether the durable watermark has been read yet. A consumer that projects rows OUTSIDE the feed (the agent-session push scan) uses it to stay behind the feed dispatch that anchors the watermark.

func (*FeedCursor) Position

func (c *FeedCursor) Position() (time.Time, string)

Position is the cached watermark — the (at, key) a consumer passes back to Commit when it wants the cursor left exactly where it is.

type FeedPass

type FeedPass[T any] struct {
	// Until is the read window's exclusive end, already held back by
	// FeedCommitLag.
	Until time.Time
	// Floor optionally starts the READ later than the durable watermark, when the
	// consumer can prove nothing before it is deliverable anyway (webhooks: the
	// oldest eligibility interval's start). A read-side skip only — nothing is
	// persisted, and the park check below still measures the durable cursor, so a
	// floor can never park the watermark forward over unread rows.
	Floor time.Time
	// Verbs is the audit arm's verb allow-list. Both audit arms filter with
	// `e.verb = ANY($4)`, so an empty Verbs matches NO verb and drops the audit
	// arm from the union entirely — it is "exclude audit rows", not "every verb".
	// That is what the push worker wants (it projects no audit source at all);
	// a consumer that does want audit events must name them.
	Verbs []string
	// Tenants scopes the read to the workspaces this consumer has recipients in.
	// Required: every arm filters with `= ANY($5)`, so an empty Tenants returns
	// zero rows — which this pager cannot tell from a quiet feed, and would answer
	// by PARKING the durable watermark past events it never read. A consumer with
	// no tenants must not run a pass at all.
	Tenants []string
	// Limit is one page's size. A full page means more may be waiting, so the
	// pass reads again immediately instead of costing a whole tick.
	Limit int
	// Park is how far the durable cursor may lag the read window before an
	// otherwise-quiet pass persists it forward; 0 => DefaultFeedPark.
	Park time.Duration

	// List reads one page after (afterAt, afterKey) — ListWebhookEvents.
	List func(ctx context.Context, afterAt time.Time, afterKey string, until time.Time, verbs, tenants []string, limit int) ([]WebhookEventRow, error)
	// Project turns one page into the consumer's batch. It runs per page, not per
	// row, so each consumer keeps its own fan-out loop (and its own clock read)
	// intact. Returning an empty batch is normal — the cursor still advances past
	// rows this consumer does not deliver.
	Project func(ctx context.Context, rows []WebhookEventRow) ([]T, error)
	// Commit inserts the batch AND advances the durable watermark to (at, key) in
	// ONE transaction — EnqueueWebhookDeliveries / EnqueuePushNotifications.
	Commit func(ctx context.Context, batch []T, at time.Time, key string) error
}

FeedPass is one pass over the feed: the window to read, the per-consumer knobs, and the three seams (read, project, commit) that make a tailer what it is. T is the consumer's own batch item — a webhook delivery, a push notification — which only its own Project and Commit ever see.

type GitConnection

type GitConnection struct {
	WorkspaceID    string    `json:"workspaceId"`
	InstallationID int64     `json:"installationId"`
	AccountLogin   string    `json:"accountLogin"`
	CreatedAt      time.Time `json:"createdAt"`
}

GitConnection is a row of `git_connections`: a GitHub App installation a workspace has connected (docs/ADR026-github-integration.md, ADR078). Since w2/m162 the key is composite (workspace_id, installation_id) — the model is N:N. A workspace holds many connections (one per GitHub account/org it has installed the App on), and an installation may serve many workspaces, each binding carrying its own complete three-proof sequence (ADR078 §2). A re-connect of the same (workspace, installation) pair upserts; anything else adds a row.

type GitConnectionLimitError

type GitConnectionLimitError struct {
	Count int
	Limit int
}

GitConnectionLimitError reports an atomic refusal of the per-workspace quota (BEX_MAX_GIT_CONNECTIONS_PER_WORKSPACE). It is typed so the GitHub service can preserve its public GIT_CONNECTION_LIMIT dialect without making this storage package depend on API errors.

func (*GitConnectionLimitError) Error

func (e *GitConnectionLimitError) Error() string

type GitHubClaimCandidate

type GitHubClaimCandidate struct {
	InstallationID int64  `json:"installationId"`
	AccountLogin   string `json:"accountLogin"`
}

GitHubClaimCandidate is one proved member of a claim selection: an installation the authorizing user was shown to administer.

type GitHubClaimSelection

type GitHubClaimSelection struct {
	ID          string
	WorkspaceID string
	Subject     string
	Candidates  []GitHubClaimCandidate
	ExpiresAt   time.Time
}

GitHubClaimSelection is an ambiguous claim's deferred choice (ADR078 §3a): the candidate set the callback ALREADY proved, held for the few minutes the human needs to pick one. Subject- and workspace-bound, single-use, and closed — the selection can only ever resolve to a member of Candidates.

type GitHubConnectTransaction

type GitHubConnectTransaction struct {
	Nonce    string
	TenantID string
	Subject  string
	// InstallationID is the claim flow's optional start-time selector (ADR078
	// §3a): 0 = unspecified. When set it NARROWS the server-proved candidate set
	// at the callback; it can never add to it, because an installation the OAuth
	// user does not administer is not a candidate regardless of what was named
	// here. Unused by the install flow, which gets its id from GitHub.
	InstallationID int64
	ExpiresAt      time.Time
}

GitHubConnectTransaction is one in-flight connect attempt: who started it, for which workspace, and until when (w1/m67 F3). It exists because the GitHub install redirect returns to an anonymous callback, so without a server-side record the flow could only ever prove that SOMEONE authorized SOME workspace — never that the human completing the installation is the one who asked.

type GitInstallationWorkspaceLimitError

type GitInstallationWorkspaceLimitError struct {
	Count int
	Limit int
}

GitInstallationWorkspaceLimitError is GitConnectionLimitError's mirror: an atomic refusal of the per-installation quota (BEX_MAX_WORKSPACES_PER_GIT_INSTALLATION, ADR078 §2). It bounds how many workspaces one installation may serve, and therefore how wide a single push delivery can fan out (ADR078 §4a).

func (*GitInstallationWorkspaceLimitError) Error

type GitWebhookReplayClaim

type GitWebhookReplayClaim struct {
	Scope    string
	KeyClass string
	Epoch    string
	Digest   string
}

GitWebhookReplayClaim is one tenant- and signing-epoch-bound body digest.

type GitWebhookReplayEpoch

type GitWebhookReplayEpoch struct {
	KeyClass string
	Epoch    string
}

GitWebhookReplayEpoch identifies one key class and exact signing secret that an API replica currently accepts.

func GitWebhookReplayEpochs

func GitWebhookReplayEpochs(manualSecret, githubSecret string) []GitWebhookReplayEpoch

GitWebhookReplayEpochs derives the active epochs from configured secrets.

type GroupingPair

type GroupingPair struct {
	Project     string
	Environment string
}

GroupingPair names one blueprint-declared project/environment grouping.

type GroupingStore

type GroupingStore interface {
	ListProjects(ctx context.Context, tenantID string) ([]Project, error)
	CreateProject(ctx context.Context, tenantID, name string) (Project, error)
	ListEnvironments(ctx context.Context, projectID string) ([]Environment, error)
	CreateEnvironment(ctx context.Context, projectID, tenantID, name string) (Environment, error)
	SetEnvironmentACL(ctx context.Context, id, protectedStatus string, networkIsolationEnabled bool, ipAllowList []core.IPAllowListEntry) error
	// CountWorkspaceGroupings returns the workspace's durable project and
	// environment totals — the quota input (w8/m20 t002). Inside a grouping
	// transaction the counts are consistent with the writes.
	CountWorkspaceGroupings(ctx context.Context, tenantID string) (projects, environments int, err error)
}

GroupingStore is the project/environment surface the Blueprint apply loop writes through — satisfied by *PGStore (pool-backed, non-transactional) and by the RunGroupingTx callback's tx-scoped facade (w8/m20 t001: a mid-loop failure then rolls back every grouping row from that sync).

type HourlyRow

type HourlyRow struct {
	WorkspaceID  string
	ServiceID    string
	Kind         string
	Tier         string    // non-empty only for instance_seconds
	ResourceKind string    // ResourceKindService / ResourceKindPostgres / ResourceKindKeyValue
	WindowStart  time.Time // truncated to the hour (UTC)
	Quantity     int64
	// SourceHealth is durable evidence for this exact window. nil means the
	// caller has no modern coverage evidence (not healthy by default).
	SourceHealth []UsageSourceObservation
}

HourlyRow is one window of usage for one resource + meter kind.

type Invite

type Invite struct {
	ID       string `json:"id"`
	TenantID string `json:"tenantId"`
	Email    string `json:"email"`
	Role     string `json:"role"`
	// Token is the plaintext capability IN FLIGHT only: set by CreateInvite and
	// RefreshInvite (the two mints) so the caller can email the link, never
	// persisted — the row stores sha256(token) (w1/041), so reads return "".
	Token      string     `json:"token"`
	InvitedBy  string     `json:"invitedBy,omitempty"`
	CreatedAt  time.Time  `json:"createdAt"`
	ExpiresAt  time.Time  `json:"expiresAt"`
	AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
}

Invite is a row of `tenant_invites` — a pending workspace membership addressed to an email that has no OpenFGA subject yet. Redeemed on the recipient's first authenticated login (AcceptInvitesForEmail) into a tenant_members row + the matching workspace:<id> tuple.

type Job

type Job struct {
	ID           string     `json:"id"`
	ServiceName  string     `json:"serviceName"`
	TenantID     string     `json:"tenantId"`
	StartCommand string     `json:"startCommand"`
	PlanID       string     `json:"planId"`
	Status       string     `json:"status"`
	CreatedAt    time.Time  `json:"createdAt"`
	StartedAt    *time.Time `json:"startedAt,omitempty"`
	FinishedAt   *time.Time `json:"finishedAt,omitempty"`
}

Job is a row of `jobs` — a one-off command run in a service's container (Render's /services/{id}/jobs). ServiceName is the bex App name (the workspace-scoped identifier the REST surface uses); TenantID scopes visibility. Status progresses pending → running → succeeded|failed|canceled.

type JobListFilter

type JobListFilter struct {
	Statuses       []string
	CreatedBefore  time.Time
	CreatedAfter   time.Time
	StartedBefore  time.Time
	StartedAfter   time.Time
	FinishedBefore time.Time
	FinishedAfter  time.Time
	Cursor         string
	Limit          int
}

JobListFilter narrows ListJobs. Limit 0 means no cap (returns all).

type MembershipGranter

type MembershipGranter interface {
	// GrantWorkspaceAdmin makes subject an admin of workspace:<tenantID> — the
	// owner of a freshly minted tenant.
	GrantWorkspaceAdmin(ctx context.Context, tenantID, subject string) error
	// GrantWorkspaceMember makes subject a developer of workspace:<tenantID> —
	// the role a minted API key gets (least privilege over every resource verb).
	GrantWorkspaceMember(ctx context.Context, tenantID, subject string) error
	// GrantWorkspaceRole grants subject an arbitrary role relation on
	// workspace:<tenantID> — how a redeemed invite (internal/api/tenancy.go)
	// seats the accepted member at its invited role (viewer/contributor/…/admin).
	GrantWorkspaceRole(ctx context.Context, tenantID, subject, relation string) error
	// RevokeWorkspaceMember removes subject's membership tuple for relation
	// (e.g. "developer" for a revoked API key) — on revoke.
	RevokeWorkspaceMember(ctx context.Context, tenantID, subject, relation string) error
}

MembershipGranter writes/removes a subject's workspace membership in OpenFGA (the authz write side). Implemented by the authz checker and injected here so the store + api packages keep no dependency on the authz client. Structural.

type NamespaceReconciler

type NamespaceReconciler struct {
	Client client.Client
	Store  Store
	Resync time.Duration
	// Identity names this control-plane instance (BEX_CP_IDENTITY). It is
	// stamped on every namespace this reconciler provisions and is the scope
	// pruneOrphans deletes within. Empty is read as DefaultControlPlaneIdentity.
	Identity string
	// contains filtered or unexported fields
}

NamespaceReconciler ensures every workspace has its per-tenant namespace(s) with base isolation objects, and prunes namespaces for deleted workspaces.

func NewNamespaceReconciler

func NewNamespaceReconciler(cl client.Client, store Store) *NamespaceReconciler

NewNamespaceReconciler builds a reconciler with the default resync period. Identity is left zero, which identity() reads as the default.

func (*NamespaceReconciler) EnsureWorkspace

func (r *NamespaceReconciler) EnsureWorkspace(ctx context.Context, workspaceID string) error

EnsureWorkspace synchronously converges one workspace's hosting and sandbox namespaces — the on-demand twin of the level-triggered ReconcileOnce. The create path calls it when a freshly minted workspace's tea-* namespace does not exist yet: nothing kicks this reconciler on workspace mint, so without it a first create within one resync period of onboarding fails with a namespace NotFound (w2/026). Failures here are NOT collected per-object the way ReconcileOnce collects per-workspace — the caller is a request waiting on this one workspace, so the first error returns immediately.

func (*NamespaceReconciler) Kick

func (r *NamespaceReconciler) Kick()

Kick schedules an immediate reconcile (non-blocking, coalescing) — called after a workspace create/delete so provisioning is not a full resync away.

func (*NamespaceReconciler) ReconcileOnce

func (r *NamespaceReconciler) ReconcileOnce(ctx context.Context) error

ReconcileOnce ensures a namespace (and its base isolation objects) for every workspace, then deletes managed namespaces whose workspace no longer exists. Per-workspace failures are collected, not fatal — one bad workspace can't block the rest, mirroring the App Reconciler's error discipline.

func (*NamespaceReconciler) Run

func (r *NamespaceReconciler) Run(ctx context.Context)

Run reconciles until ctx is done.

type NotificationSettings

type NotificationSettings struct {
	ID              string          `json:"id"`
	TenantID        string          `json:"tenantId"`
	Subject         string          `json:"subject"`
	DeployStarted   bool            `json:"deployStarted"`
	DeploySucceeded bool            `json:"deploySucceeded"`
	DeployFailed    bool            `json:"deployFailed"`
	PushPolicy      json.RawMessage `json:"-"`
	CreatedAt       time.Time       `json:"createdAt"`
	UpdatedAt       time.Time       `json:"updatedAt"`
}

NotificationSettings is a row of `notification_settings` — one member's override of their per-workspace deploy-email preferences (w3/m9). A member with no row is not "opted out" — internal/notifications.Service applies the failure-only default when GetNotificationSettings returns ErrNotFound.

type NotifyRecipient

type NotifyRecipient struct {
	Subject         string
	DeployStarted   bool
	DeploySucceeded bool
	DeployFailed    bool
}

NotifyRecipient is one workspace member's resolved deploy-notification preferences — an explicit notification_settings row's values, or the default (started=false, succeeded=false, failed=true) for a member who never customized them. Returned by ListNotifyRecipients, the notification service's fan-out source: it names WHO to consider emailing, not their address (email resolution is the identity provider's job, outside the store).

type ObservedDatastoreState

type ObservedDatastoreState struct {
	DatastoreID string
	WorkspaceID string
	Kind        string
	At          time.Time
	Phase       string
	// Suspended is spec.suspended at observation time. Only the user-driven
	// Suspend verb writes it, so it separates intentional downtime from an
	// outage — a suspended datastore is observed availability-empty, never
	// unavailable (the hibernated-App precedent).
	Suspended            bool
	Availability         string
	AvailabilityObserved bool
	ReasonCode           string
	ReadyTransitionAt    time.Time
	// AvailabilitySuppressed is the App snapshot's flag: a guard blanked this
	// pass's conclusion, so it leaves the checkpoint's updated_at alone.
	AvailabilitySuppressed bool

	// LastBackup* mirror Database.status.lastBackup for t002 edge detection.
	// Empty Name means the operator has not yet projected a terminal backup.
	LastBackupName      string
	LastBackupPhase     string // completed | failed
	LastBackupError     string
	LastBackupScheduled bool
	LastBackupAt        time.Time

	// Recovering is true when Spec.Recovery.SourceBackupServerName is set —
	// the Database was created as a restore target (ADR009).
	Recovering bool

	// SpecVersion / CurrentVersion drive major-upgrade edges. UpgradeFailed
	// is Ready=False with reason MajorVersionUpgradeFailed.
	SpecVersion    string
	CurrentVersion string
	UpgradeFailed  bool

	// Checkpoint-only extras round-tripped by the memStore (PG keeps them in
	// dedicated columns). Producers leave them empty; RecordObserved* fills them.
	RestoreOutcome string
	UpgradeKey     string
}

ObservedDatastoreState is the small level-triggered snapshot the control-plane reconciler derives from typed Database/KeyValue status — ObservedServiceState with the datastore's own identity (its immutable dpg-/red- metadata.name plus the workspace its CR label carries, since a datastore has no apps row to join through). Availability is empty outside a running datastore, healthy, or unhealthy, and carries the same AvailabilityObserved / ReadyTransitionAt contract the App snapshot documents.

type ObservedServiceState

type ObservedServiceState struct {
	AppID        string
	At           time.Time
	ServicePhase string
	// Suspended is the App's spec.suspended at observation time — the ONLY
	// signal that separates a user-driven suspension from free-tier idle
	// auto-hibernation, since both observe the same Hibernated phase (w6/m47).
	Suspended    bool
	Availability string
	// AvailabilityObserved distinguishes "do not advance this dimension" from
	// the observed empty state used while a service is hibernated.
	AvailabilityObserved bool
	ReasonCode           string
	InstanceID           string
	// ReadyTransitionAt is the Ready condition's LastTransitionTime backing
	// this availability conclusion (w6/m41) — the operator-side timestamp the
	// reconciler's stale-conclusion guard orders an unhealthy edge against the
	// last recorded healthy checkpoint, and the value recorded as that
	// checkpoint's healthy_transition_at when the conclusion is healthy. Zero
	// when availability was not derived from a Ready condition (e.g.
	// hibernation) or the condition carried no timestamp; the guard treats
	// zero as "cannot order" and fails open toward recording. A crash under an
	// open deploy carries the Serving condition's stop instead when it is later
	// than Ready's (the rollout start), and both the edge and the stale guard
	// use it (w5/083).
	ReadyTransitionAt time.Time
	// AvailabilitySuppressed marks a pass whose availability conclusion a
	// reconciler guard blanked: the debounce's first unhealthy pass, or a
	// stale re-read (suppressAvailability). Its phase still records, but it
	// leaves the checkpoint's updated_at alone — the conclusion it carried
	// began before this pass, so this pass is no floor for the next edge. It
	// cannot be inferred from AvailabilityObserved: a pass that concluded
	// nothing (a rollout in progress) is unobserved too, and its phase change
	// must move the floor.
	AvailabilitySuppressed bool
}

ObservedServiceState is the small level-triggered snapshot the control-plane reconciler derives from typed App status. Availability is empty outside a running service, healthy, or unhealthy.

type PGStore

type PGStore struct {
	Pool *pgxpool.Pool
	// OpsWorkspaceID pins the platform ops workspace (BEX_OPS_WORKSPACE,
	// docs/ADR088-platform-observability-ui.md §4). Two store-level guards key
	// on it: invite redemption exempts it from seat/plan gating
	// (planAllowsJoin — a cap must never silently block onboarding an
	// operator), and account-deletion disposition classifies a sole-member ops
	// workspace as blocked rather than delete (its teardown would lock every
	// operator out of the observability UI). Empty (unset) => both guards inert.
	OpsWorkspaceID string
	// contains filtered or unexported fields
}

PGStore is the Postgres-backed Store over a pgx pool. It holds no business logic — validation happens in the API layer, classification of Postgres errors happens here.

func NewPGStore

func NewPGStore(pool *pgxpool.Pool) *PGStore

func (*PGStore) AbandonAgentDispatch

func (s *PGStore) AbandonAgentDispatch(ctx context.Context, d AgentDispatch, now time.Time, failure AgentSessionFailure) (TerminalTurnFact, error)

AbandonAgentDispatch serializes against binding and atomically settles only the accepted turn that owns the intent. A canceled/deleted/newer session still leaves a cleanup tombstone, but its lifecycle is never overwritten. When the session row is terminalized (still unbound creating/redispatching), the returned TerminalTurnFact carries timing for metrics (w5/m88); a zero Turn means no session terminalization occurred (tombstone-only or already bound — the latter returns ErrNotFound).

func (*PGStore) AbandonBlueprintSync

func (s *PGStore) AbandonBlueprintSync(ctx context.Context, runID string, now time.Time, reason string) (bool, error)

AbandonBlueprintSync settles one stale running run as interrupted (w8/m37 t004 + w8/m39). The run settles only from running; the Blueprint flips to error only while the abandoned generation still owns the claim (or for a legacy row stuck syncing with no claim). Clearing the claim also bumps execution_generation so a paused worker cannot restamp ownership or pass AssertBlueprintExecution after recovery frees the row — expiry alone is not proof of death, but the generation bump is the fence. Disconnected rows and newer generations are never overwritten. It returns false when another writer settled first — an idempotent no-op, not an error.

func (*PGStore) AbortRehydrate

func (s *PGStore) AbortRehydrate(ctx context.Context, id string) (AgentSession, error)

AbortRehydrate reverts a rehydrate whose background sandbox provisioning failed (ADR059 D4), CAS-moving resuming → hibernated so the snapshot (still present) can be retried by a later Resume. Guarded to a still-snapshot-bearing resuming row so it can't clobber a session that already rehydrated or was canceled.

func (*PGStore) AcceptInviteByToken

func (s *PGStore) AcceptInviteByToken(ctx context.Context, token, subject string) (Invite, error)

AcceptInviteByToken redeems ONE invite by its emailed token for subject — the direct-accept path (w1/m33) that makes the invite link real: the recipient may have signed up under a DIFFERENT email than the one invited, which the login-time email match (AcceptInvitesForEmail) can never redeem. The token is the capability; possession of the link is the authorization. The lookup is by sha256(token) — only the hash is at rest (w1/041). Named refusals rather than a silent no-op: unknown token is ErrNotFound, an already-accepted or expired invite is ErrConflict (the caller can say WHY the link failed), and a workspace whose current plan cannot seat the invite refuses with ErrConflict exactly like the login path's silent skip — except here the caller is told, because they asked explicitly.

func (*PGStore) AcceptInvitesForEmail

func (s *PGStore) AcceptInvitesForEmail(ctx context.Context, email, subject string) ([]Invite, error)

AcceptInvitesForEmail redeems every outstanding invite addressed to email into a tenant_members row for subject, marking each invite accepted — the whole set in ONE transaction so a signup either joins all its invited workspaces or none (a partial join with dangling invites would be confusing to re-drive). It returns the accepted invites so the caller can write the matching OpenFGA tuples (Postgres and OpenFGA aren't one transaction; the row is the source of truth, the tuple a best-effort follow-up the resolver re-drives). Idempotent: a second login finds no outstanding invites and returns an empty slice. An invite for someone who already belongs to the workspace is still marked accepted (so it stops lingering) but leaves their role untouched — see redeemInvite; each returned invite carries the EFFECTIVE membership role.

func (*PGStore) AcceptPushDelivery

func (s *PGStore) AcceptPushDelivery(ctx context.Context, delivery DuePushDelivery, ticketID string, at, receiptDue time.Time) (bool, error)

AcceptPushDelivery records provider acceptance only (not device receipt). expectedClaim prevents a late worker from completing another worker's lease.

func (*PGStore) AccountDeletionTombstoned

func (s *PGStore) AccountDeletionTombstoned(ctx context.Context, subject string) (bool, error)

func (*PGStore) AddDomain

func (s *PGStore) AddDomain(ctx context.Context, appID, host, redirectForName string) error

AddDomain appends or updates a non-primary domain row for apps.IntentStore. Re-adding a host this app already owns updates redirect_for_name (used when an auto-paired sibling becomes explicit); another app owning it is a real cross-app conflict.

func (*PGStore) AddDomainClaim

func (s *PGStore) AddDomainClaim(ctx context.Context, appID, host, redirectForName string) (Domain, bool, error)

AddDomainClaim creates one globally-unique pending claim. A same-App retry returns the exact existing row and challenge, updating only redirect intent; a claim owned by another App remains a conflict regardless of claim state.

func (*PGStore) AddMember

func (s *PGStore) AddMember(ctx context.Context, subject, tenantID, role string) error

AddMember records a PERSON's membership in a tenant (kind defaults to 'user') — the platform tenant-create path (store/api.go, an explicit Admin) and the invite-acceptance paths. API-key bindings do NOT come through here: BindClient writes its own row marked kind='machine' (w5/m103), because the two are the same table and shape but not the same thing.

func (*PGStore) AdmitBlueprintCreate

func (s *PGStore) AdmitBlueprintCreate(ctx context.Context, b Blueprint, run BlueprintSync) (Blueprint, BlueprintSync, bool, error)

AdmitBlueprintCreate atomically upserts the Blueprint row for explicit creation and claims its initial sync: a conflicting active claim loses with ErrBlueprintSyncBusy, while a disconnected row is deliberately re-established under a fresh (bumped) generation so old queued execution cannot regain authority (w8/m37 t001). The run insert commits in the same transaction — admission failure means zero workload mutations.

The third return value reports that a DISCONNECTED row was revived — a new connection on an old row (w4/120). It matters because the row keeps its id and its sync history across the boundary, so the caller can say so; and because created_at is reset for exactly that case below. Reviving is the deliberate w8/m37 behavior and is not changed here: what changed is that a connection made seconds ago no longer reports an age of two days.

func (*PGStore) AdmitBlueprintSyncRun

func (s *PGStore) AdmitBlueprintSyncRun(ctx context.Context, blueprintID, tenantID string, run BlueprintSync) (Blueprint, BlueprintSync, error)

AdmitBlueprintSyncRun atomically claims the active apply for an existing Blueprint and records its running run: generation bump, active_run_id claim, syncing status, and the run insert commit together. The caller must have resolved the row through the active lookup first, so zero claimed rows means a competing admission won the race → ErrBlueprintSyncBusy, never a silent second apply.

func (*PGStore) AdvanceAccountDeletion

func (s *PGStore) AdvanceAccountDeletion(ctx context.Context, subject, from, to string) error

func (*PGStore) AdvanceEnvGroupOp

func (s *PGStore) AdvanceEnvGroupOp(ctx context.Context, id, opID, phase string, leaseUntil time.Time) error

AdvanceEnvGroupOp records a later phase (and a renewed lease) for opID. It does not move the content version, so the claim's release still matches.

func (*PGStore) AgentSessionSSHActivity

func (s *PGStore) AgentSessionSSHActivity(ctx context.Context, resourceID string, freshSince time.Time) (hasFreshOpen bool, lastEnded *time.Time, err error)

AgentSessionSSHActivity summarizes a resource's editor-SSH activity for the agent-session idle reaper (ADR059 D2 / w2/m67): whether a still-open session (ended_at IS NULL) started at or after `freshSince` exists — a live editor that pins the sandbox and must never be reaped — and the most recent close time (ended_at), which feeds the idle clock `now − max(last turn end, last SSH disconnect)`. `freshSince` (now − the SSH session cap) discards a leaked row a crashed gateway replica never closed, so an editor pin stays bounded. It subsumes ADR054 D6's earlier open-only check.

func (*PGStore) AgentSessionTranscript

func (s *PGStore) AgentSessionTranscript(ctx context.Context, sessionID string, afterSeq int64, maxBytes int64, limit int) ([]AgentSessionTranscriptPart, error)

AgentSessionTranscript returns a session's stored parts in emission order, strictly after afterSeq (pass -1 for the whole transcript), capped at maxBytes of cumulative payload: rows are scanned in seq order and the read stops before the part that would exceed the budget, so a replay never materializes more than maxBytes even if the stored transcript ever outgrew it. limit additionally bounds the ROW count in SQL (ADR065 D2's poll-shaped page; <=0 ⇒ unbounded, the gateway replay's paging is byte-budgeted). It is the replay source for reattach and for terminal-session history (ADR047 D9): it reads the durable store, never the sandbox, so it works after the sandbox is gone.

func (*PGStore) AgentSessionTranscriptBytes

func (s *PGStore) AgentSessionTranscriptBytes(ctx context.Context, sessionID string) (int64, error)

AgentSessionTranscriptBytes returns the total stored payload bytes of a session's transcript. Write paths seed their cumulative quota counter from it so the stored transcript can never exceed MaxAgentSessionTranscriptBytes regardless of how many turns or attaches append.

func (*PGStore) AgentSessionTranscriptMaxSeq

func (s *PGStore) AgentSessionTranscriptMaxSeq(ctx context.Context, sessionID string) (int64, bool, error)

AgentSessionTranscriptMaxSeq returns the highest stored seq for a session and whether any part exists. The gateway uses it to skip driver-replayed parts it has already persisted, so its live tee resumes exactly where the store ends.

func (*PGStore) AgentSessionTranscriptProgress

func (s *PGStore) AgentSessionTranscriptProgress(ctx context.Context, sessionID string, turn int, indexes []int64) (int64, []int64, error)

AgentSessionTranscriptProgress reads cumulative bytes and existing requested ordinals from one snapshot. The result is bounded by indexes, not transcript size. Historical concurrent tees may leave holes, so max(part_index) alone cannot tell a harvest which parts are duplicates.

func (*PGStore) AgentSessionTranscriptTurnMaxIndex

func (s *PGStore) AgentSessionTranscriptTurnMaxIndex(ctx context.Context, sessionID string, turn int) (int64, bool, error)

AgentSessionTranscriptTurnMaxIndex returns the largest stored local driver ordinal for one turn. A reconnect uses it to skip the prefix already replayed from Postgres while still accepting a fresh sandbox's ordinal zero.

func (*PGStore) AgentSessionTurns

func (s *PGStore) AgentSessionTurns(ctx context.Context, sessionID string) ([]AgentSessionTurn, error)

AgentSessionTurns returns the durable user intents in chronological order. Prompt input is capped individually and by MaxAgentSessionPromptBytes across the session; callers still stream assistant parts page-wise.

func (*PGStore) AppendAgentSessionTranscript

func (s *PGStore) AppendAgentSessionTranscript(ctx context.Context, sessionID string, parts []AgentSessionTranscriptPart) error

AppendAgentSessionTranscript idempotently persists teed transcript parts. Driver ordinals restart at zero for every sandbox, so identity is the turn-local (session_id, turn, part_index). seq remains a monotonic per-session cursor allocated while holding a transaction-scoped advisory lock; concurrent gateway replicas therefore cannot collide or reorder the replay cursor.

func (*PGStore) AssertBlueprintExecution

func (s *PGStore) AssertBlueprintExecution(ctx context.Context, id, tenantID string, generation int64, runID string) error

AssertBlueprintExecution confirms the caller's admitted (generation, runID) still owns the Blueprint claim (w8/m39). Callers invoke it before each resource-mutation family so a retired worker cannot start further side effects. Zero rows → ErrBlueprintSyncBusy. The same WHERE shape as Stage / Complete is the sink precondition: a Get-then-act race loses here rather than writing under a freed claim.

func (*PGStore) BackfillAppType

func (s *PGStore) BackfillAppType(ctx context.Context, id, serviceType string) (bool, error)

BackfillAppType heals a pre-w6/m46 row that predates the type column. Only matching an empty type is the whole safety story: it makes the write idempotent, race-free between concurrent projector passes, and incapable of changing a type the create path already recorded (spec.type is immutable, so a disagreement would be corruption, not an update).

func (*PGStore) BeginAccountDeletion

func (s *PGStore) BeginAccountDeletion(ctx context.Context, subject, email string, machineSubjects []string) (AccountDeletion, error)

BeginAccountDeletion serializes against membership mutations with the same per-workspace advisory locks they use, rechecks disposition, and writes the tombstone plus immutable workspace plan in one transaction.

func (*PGStore) BeginAgentSessionTurn

func (s *PGStore) BeginAgentSessionTurn(ctx context.Context, id, prompt, deliveryMode, phase, status string) (AgentSession, error)

BeginAgentSessionTurn atomically records an accepted follow-up prompt and moves the session into its asynchronous dispatch phase. The public turn count advances here, with prompt acceptance, so provisioning failure cannot erase or renumber what the user submitted.

func (*PGStore) BeginRehydrate

func (s *PGStore) BeginRehydrate(ctx context.Context, id, prompt, deliveryMode string) (AgentSession, error)

BeginRehydrate claims a hibernated session for rehydration on Resume/Steer (ADR059 D4), CAS-moving it hibernated → resuming while KEEPING the snapshot fields (the fresh sandbox is provisioned in the background; the snapshot is only consumed once it is up). A concurrent retention delete or double resume finds no `hibernated` row (ErrNotFound). The row's sandbox_id stays empty until RehydrateAgentSession adopts the new one.

func (*PGStore) BillingCustomerID

func (s *PGStore) BillingCustomerID(ctx context.Context, workspaceID string, livemode bool) (string, bool, error)

BillingCustomerID returns the persisted same-mode Customer for a workspace. A mapping from the other Stripe mode is invisible: its Customer cannot be retrieved with the current key.

func (*PGStore) BillingExportReport

func (s *PGStore) BillingExportReport(ctx context.Context, workspaceID string, start, end time.Time) (BillingExportReport, error)

func (*PGStore) BillingExportStats

func (s *PGStore) BillingExportStats(ctx context.Context, floor, sealBefore, now time.Time, requirePaymentMethod bool) (BillingExportStats, error)

func (*PGStore) BindClient

func (s *PGStore) BindClient(ctx context.Context, clientID, tenantID string) error

BindClient records that an API key belongs to a tenant: a tenant_members row with the key's client_id as subject and role "developer" — least privilege that still covers every resource verb, matching the FGA grant apikeys mints (w1/m9). The PK is (tenant_id, subject), so a rebind to a DIFFERENT tenant is not a PK conflict — delete any existing binding for this client first, in the same transaction, so a client is bound to at most one tenant.

func (*PGStore) BindGitConnection

func (s *PGStore) BindGitConnection(ctx context.Context, c GitConnection, maxConnections, maxWorkspaces int) (GitConnection, error)

BindGitConnection performs the same-pair check, both quota checks, and the write in one transaction. The transaction-scoped advisory locks work across API replicas and distinct pool connections.

SECURITY: the counts and the insert MUST share this transaction. A standalone count beforehand lets two concurrent callbacks at limit-1 both pass. Two locks are taken because N:N has two independent caps — the workspace's fan-in and the installation's fan-out — and they are always acquired workspace-first so concurrent binds cannot deadlock on opposite orders. A same-pair reconnect is detected before either count and remains exempt from both.

func (*PGStore) BlueprintResourceOwners

func (s *PGStore) BlueprintResourceOwners(ctx context.Context, tenantID string) (map[BlueprintResourceKey]string, error)

BlueprintResourceOwners maps every claimed resource in the workspace to its owning blueprint id, so an ownership check over a whole manifest costs one query rather than one per declared resource (w5/114).

func (*PGStore) BlueprintWorkspace

func (s *PGStore) BlueprintWorkspace(ctx context.Context, id string) (string, error)

BlueprintWorkspace returns the workspace that owns a live (non-disconnected) Blueprint id, or ErrNotFound. It is a routing read only: the caller must still authorize against the returned workspace before revealing anything, exactly as a service or env-group id resolves to its own workspace (w4/m169).

func (*PGStore) BuiltDeployIDs

func (s *PGStore) BuiltDeployIDs(ctx context.Context, appID string, limit int) (map[string]bool, error)

BuiltDeployIDs is the set of an App's deploys that actually ran a build — the deploys the Events tab shows a Build started entry for. The deploy-log narrator reads it so it cannot tell a build story for a deploy the Events feed has no build record of (w4/m110 t004): a rollback and a rollout that reuses the active artifact both open a deploy row with real timestamps, and timestamps alone cannot distinguish them from a build.

Scoped to one App and capped like every other feed read. An App with no build facts yields an empty (non-nil) set, never an error.

func (*PGStore) BumpOAuthRevocation

func (s *PGStore) BumpOAuthRevocation(ctx context.Context, subject, clientID string) error

BumpOAuthRevocation advances the durable invalidation marker shared by every bex-api replica. clock_timestamp() ensures two bumps in one transaction are ordered by wall time rather than the transaction start time.

func (*PGStore) CancelWorkspaceCreationAttempt

func (s *PGStore) CancelWorkspaceCreationAttempt(ctx context.Context, id, subject string) error

func (*PGStore) CheckBillingMutationAllowed

func (s *PGStore) CheckBillingMutationAllowed(ctx context.Context, workspaceID string) error

CheckBillingMutationAllowed is the feature-service gate for billable creates, upgrades, deploys, and tenant resume attempts. Grace remains usable; the gate begins only once enforcement is due/active.

func (*PGStore) ClaimAccountDeletions

func (s *PGStore) ClaimAccountDeletions(ctx context.Context, limit int) ([]AccountDeletion, error)

func (*PGStore) ClaimAgentSessionForHibernation

func (s *PGStore) ClaimAgentSessionForHibernation(ctx context.Context, id string) (AgentSession, error)

ClaimAgentSessionForHibernation atomically claims a finished session for the hibernation reclaim (ADR059 D3), moving it from a terminal phase holding a sandbox into the transient `hibernating` phase so exactly one Completer replica runs the snapshot. It is CAS-guarded like the reap path: only a completed/failed row with a live sandbox_id matches, so a concurrent Cancel or a second replica finds no row (ErrNotFound) and backs off. Returns the claimed row (its sandbox_id still set — the snapshot exec needs it).

func (*PGStore) ClaimBillingNotifications

func (s *PGStore) ClaimBillingNotifications(ctx context.Context, now time.Time, lease time.Duration, limit int) ([]BillingNotification, error)

func (*PGStore) ClaimBlueprintAutoSyncIntents

func (s *PGStore) ClaimBlueprintAutoSyncIntents(ctx context.Context, limit int, lease time.Duration) ([]BlueprintAutoSyncIntent, error)

ClaimBlueprintAutoSyncIntents leases a bounded batch of pending (or stale-claimed) intents for the worker. lease is how long a claimed row stays exclusive before another replica may reclaim it after process loss.

func (*PGStore) ClaimBlueprintResource

func (s *PGStore) ClaimBlueprintResource(ctx context.Context, tenantID string, kind BlueprintClaimKind, name, blueprintID, expectedOwner string) error

ClaimBlueprintResource inserts or keeps ownership of (tenant, kind, name). expectedOwner "" adopts only when the row is absent or already ours; non-empty authorizes transfer only while that owner still holds the claim (intervening takeover cannot use a stale confirmation).

func (*PGStore) ClaimDueBillingLifecycle

func (s *PGStore) ClaimDueBillingLifecycle(ctx context.Context, now time.Time, lease time.Duration) (BillingLifecycle, bool, error)

ClaimDueBillingLifecycle leases one enforcement/recovery row. FOR UPDATE SKIP LOCKED makes the claim safe across bex-api replicas.

func (*PGStore) ClaimDuePushDeliveries

func (s *PGStore) ClaimDuePushDeliveries(ctx context.Context, now, leaseUntil time.Time, limit int) ([]DuePushDelivery, error)

ClaimDuePushDeliveries leases disjoint rows across worker replicas. A lease is not an attempt; a crash makes the row visible again after leaseUntil.

func (*PGStore) ClaimDuePushReceipts

func (s *PGStore) ClaimDuePushReceipts(ctx context.Context, now, leaseUntil time.Time, limit int) ([]DuePushDelivery, error)

func (*PGStore) ClaimDueWebhookAttempts

func (s *PGStore) ClaimDueWebhookAttempts(ctx context.Context, now, leaseUntil time.Time, limit int) ([]DueWebhookAttempt, error)

ClaimDueWebhookAttempts leases disjoint pending reservations across worker replicas. The attempt identity already exists before the claim; a crashed worker leaves it pending and reclaimable after leaseUntil rather than inventing evidence for a network exchange that might never have happened.

func (*PGStore) ClaimEnvGroupContent

func (s *PGStore) ClaimEnvGroupContent(ctx context.Context, id string, expectedVersion int64, op EnvGroupOp) (int64, error)

ClaimEnvGroupContent marks the group busy for op if its content version is still expectedVersion, recording the operation in the same statement. Returns the claim's content version.

func (*PGStore) ClaimGitWebhookDelivery

func (s *PGStore) ClaimGitWebhookDelivery(ctx context.Context, claim GitWebhookReplayClaim) (bool, error)

ClaimGitWebhookDelivery atomically claims a signed body within its tenant and signing-key epoch. A per-scope advisory transaction lock makes the count-before-insert cap exact even when distinct deliveries arrive on several replicas concurrently. The digest remains globally unique for rolling-upgrade compatibility with pre-0104 replicas and because the signed body itself embeds the installation/repository identity.

func (*PGStore) ClaimRoleReconciliations

func (s *PGStore) ClaimRoleReconciliations(ctx context.Context, limit int) ([]RoleReconciliation, error)

ClaimRoleReconciliations leases due rows before returning them. SKIP LOCKED partitions work safely across bex-api replicas; exact-role writes remain idempotent if a worker dies after applying OpenFGA but before acknowledging.

func (*PGStore) ClaimShellNonce

func (s *PGStore) ClaimShellNonce(ctx context.Context, nonce string, expiresAt time.Time) (bool, error)

ClaimShellNonce atomically claims a web-shell exec-ticket nonce across ALL gateway replicas (w1/042 L7): INSERT … ON CONFLICT DO NOTHING lets exactly one claimant win; a second redemption of the same ticket — on any replica — finds the row present and is refused. Expired rows are pruned here rather than by a janitor: shells are human-driven and rare, so the piggybacked DELETE keeps the table at "tickets minted in the last ~90s" for free.

func (*PGStore) ClaimUnboundBillingCustomers

func (s *PGStore) ClaimUnboundBillingCustomers(ctx context.Context, livemode bool, horizon time.Duration, limit int) ([]BillingProviderMapping, error)

ClaimUnboundBillingCustomers claims a bounded batch of same-mode mappings that never bound a payment method, are not comped, have not opened Checkout within the horizon, and were not examined in the last day. The claim stamps reclaim_checked_at, so a kept mapping waits a day before it is re-read.

func (*PGStore) ClaimWebhookFailureNotice

func (s *PGStore) ClaimWebhookFailureNotice(ctx context.Context, endpointID string, now, threshold time.Time) (bool, error)

ClaimWebhookFailureNotice compare-and-sets the endpoint's notified_at marker: it succeeds (returns true, records `now`) only when no notice has been sent since `threshold` (now − suppression window), so across replicas and restarts exactly one worker emails per window (w1/m58). NULL notified_at (never notified, or cleared on re-enable) always claims.

func (*PGStore) CleanupAccountSubject

func (s *PGStore) CleanupAccountSubject(ctx context.Context, subject, marker string) error

CleanupAccountSubject removes active personal state and anonymizes retained workspace/audit provenance. It runs only after workspace disposition.

func (*PGStore) ClearAgentSessionSandbox

func (s *PGStore) ClearAgentSessionSandbox(ctx context.Context, id string) error

ClearAgentSessionSandbox blanks a session's sandbox_id after its sandbox has been torn down (ADR054 D6), dropping it from the deferred-teardown reaper's working set and releasing the live-sandbox unique index. It never changes phase/status — the session stays terminal — and is a no-op if already blank.

func (*PGStore) ClearPreviousSandboxTeardown

func (s *PGStore) ClearPreviousSandboxTeardown(ctx context.Context, sessionID string, turn int) error

ClearPreviousSandboxTeardown blanks previous_sandbox_id on a dispatch intent after the predecessor sandbox is confirmed gone (w5/m99 t004). When the intent is an abandoned cleanup-only tombstone with no other work, the row is deleted so the table does not grow without bound.

func (*PGStore) CloseDeploy

func (s *PGStore) CloseDeploy(ctx context.Context, id, status, resolvedImage string) (bool, error)

func (*PGStore) CloseUsageSourceStreams

func (s *PGStore) CloseUsageSourceStreams(ctx context.Context, ref UsageResourceRef, through time.Time) error

CloseUsageSourceStreams ends one resource's coverage streams at through. A deleted resource's last health write reopens its streams, and the inventory reconcile would close them at the current boundary rather than its deletion hour, leaving phantom gaps after a catch-up (w4/m173).

func (*PGStore) CompactUsage

func (s *PGStore) CompactUsage(ctx context.Context, before time.Time) (UsageCompaction, error)

CompactUsage folds every usage_hourly row with window_start < before into usage_monthly and purges it (docs/ADR023-usage-metering.md § Retention). A single statement, so purge and aggregate are atomic and a re-run is a no-op; the additive ON CONFLICT means a straggler row compacted on a later pass adds to its month rather than overwriting it.

func (*PGStore) CompleteAgentSessionTurn

func (s *PGStore) CompleteAgentSessionTurn(ctx context.Context, sessionID string, turn int, complete, truncated bool, reason string) error

CompleteAgentSessionTurn records whether the persisted assistant transcript is complete. A false complete value is intentional: the row still preserves the accepted user prompt while making loss/truncation visible to consumers.

func (*PGStore) CompleteAppCreation

func (s *PGStore) CompleteAppCreation(ctx context.Context, id string) error

CompleteAppCreation releases an explicitly prepared initial configuration. Repeating completion is safe; a concurrently deleted row is never recreated.

func (*PGStore) CompleteBillingLifecycleWork

func (s *PGStore) CompleteBillingLifecycleWork(ctx context.Context, workspaceID string, expectedVersion int64, status string, now time.Time) (BillingLifecycle, error)

func (*PGStore) CompleteBillingNotification

func (s *PGStore) CompleteBillingNotification(ctx context.Context, workspaceID string, version int64, at time.Time) error

func (*PGStore) CompleteBlueprintAutoSyncIntent

func (s *PGStore) CompleteBlueprintAutoSyncIntent(ctx context.Context, id string) error

CompleteBlueprintAutoSyncIntent marks a claimed intent successfully processed.

func (*PGStore) CompleteBlueprintSync

func (s *PGStore) CompleteBlueprintSync(ctx context.Context, id, tenantID, runID string, generation int64, state string, completedAt time.Time, errMsg *string) (Blueprint, error)

CompleteBlueprintSync commits a run's terminal state together with the Blueprint status projection, computed from the CURRENT row (w8/m37 t005): success lands in_sync unless auto-sync is currently off (paused); failure lands error unless auto-sync is currently off (paused). The run settles only from running under the admitted generation, and the Blueprint updates only while the admitted generation still owns the claim on a connected row — a stale completion cannot overwrite a newer run or a disconnected row, and a failed persistence never reports success.

func (*PGStore) CompletePushDelivery

func (s *PGStore) CompletePushDelivery(ctx context.Context, delivery DuePushDelivery, at time.Time) (bool, error)

CompletePushDelivery marks a Web Push send delivered without a receipt poll.

func (*PGStore) CompleteRoleReconciliation

func (s *PGStore) CompleteRoleReconciliation(ctx context.Context, tenantID, subject, role string) error

CompleteRoleReconciliation acknowledges only the role that was applied. A concurrent newer role upsert remains queued instead of being deleted by a stale worker.

func (*PGStore) CompleteWebhookAttempt

func (s *PGStore) CompleteWebhookAttempt(ctx context.Context, completion WebhookAttemptCompletion) (bool, error)

CompleteWebhookAttempt performs the pending -> terminal transition exactly once and updates the logical notification in the same transaction. A failed automatic send reserves its next retry atomically. A failed manual send restores the automatic reservation it parked, without consuming the parent's automatic attempt_count; manual success closes the notification and drops it.

func (*PGStore) ConsumeGitHubClaimSelection

func (s *PGStore) ConsumeGitHubClaimSelection(ctx context.Context, id string) (GitHubClaimSelection, error)

ConsumeGitHubClaimSelection atomically claims an unexpired selection and returns it. Single-use by construction: the row is DELETEd in the same statement that reads it, so a replayed selection — on any replica — finds nothing.

func (*PGStore) ConsumeGitHubConnectTransaction

func (s *PGStore) ConsumeGitHubConnectTransaction(ctx context.Context, nonce string) (GitHubConnectTransaction, error)

ConsumeGitHubConnectTransaction atomically claims an unexpired attempt and returns it. Single-use by construction: the row is DELETEd in the same statement that reads it, so a replayed callback — on any replica — finds nothing. ErrNotFound covers unknown, already-consumed, and expired alike, so a caller cannot distinguish them (and neither can an attacker probing).

func (*PGStore) CountAppsForTenant

func (s *PGStore) CountAppsForTenant(ctx context.Context, tenantID string) (int, error)

CountAppsForTenant counts a workspace's apps (all of them, including suspended — the `apps` row exists whether or not spec.suspended) for the per-plan service cap.

func (*PGStore) CountEnvGroups

func (s *PGStore) CountEnvGroups(ctx context.Context, tenantID string) (int, error)

CountEnvGroups is the workspace's group count, for the create quota.

func (*PGStore) CountGitConnections

func (s *PGStore) CountGitConnections(ctx context.Context, workspaceID string) (int, error)

CountGitConnections returns how many connections a workspace holds — the per-workspace quota check (BEX_MAX_GIT_CONNECTIONS_PER_WORKSPACE, ADR075 §2).

func (*PGStore) CountInvites

func (s *PGStore) CountInvites(ctx context.Context, tenantID string) (int, error)

CountInvites counts a workspace's OUTSTANDING invites (unaccepted, unexpired) — the count the seat-cap check consults, so the Invite verb doesn't SELECT full rows just to length them. Mirrors CountTenantMembers.

func (*PGStore) CountLiveAgentSessionSandboxes

func (s *PGStore) CountLiveAgentSessionSandboxes(ctx context.Context, workspaceID string, phases []string) (int, error)

CountLiveAgentSessionSandboxes counts the workspace's sessions currently in a live phase (`phases`) that hold a sandbox id — the concurrent live-sandbox working set the ADR059 D6 per-workspace cap bounds. A terminal session in its idle grace still carries a sandbox_id but is not in a live phase, so it does not count against the create/steer/resume cap. Trusted read used by the dispatch-time admission check; a small best-effort overshoot is acceptable (the dispatch CAS and reconcile bound it), so it takes no lock.

func (*PGStore) CountPinnedAgentSessions

func (s *PGStore) CountPinnedAgentSessions(ctx context.Context, workspaceID string) (int, error)

CountPinnedAgentSessions counts a workspace's pinned sessions — the ADR059 D5 pin-quota dimension. Best-effort like the live cap; no lock.

func (*PGStore) CountRegistryCredentials

func (s *PGStore) CountRegistryCredentials(ctx context.Context, workspaceID string) (int, error)

CountRegistryCredentials returns how many stored registry credentials the workspace owns. It backs the feature's per-workspace admission quota.

func (*PGStore) CountTenantAdmins

func (s *PGStore) CountTenantAdmins(ctx context.Context, tenantID string) (int, error)

CountTenantAdmins counts a workspace's admin members — the guard the role-change/remove verbs consult so the last admin can't demote or remove itself, leaving a workspace nobody can administer.

func (*PGStore) CountTenantMembers

func (s *PGStore) CountTenantMembers(ctx context.Context, tenantID string) (int, error)

CountTenantMembers counts a workspace's members — the guard w4/m12's invite verb consults before adding one (Hobby's single-member cap).

func (*PGStore) CountUnreadPushNotifications

func (s *PGStore) CountUnreadPushNotifications(ctx context.Context, tenantID, subject string, excludeEvents []string) (int64, error)

CountUnreadPushNotifications counts the caller's unread inbox items under the same excludeEvents filter as ListOwnPushNotifications.

func (*PGStore) CountWorkspaceDomainClaims

func (s *PGStore) CountWorkspaceDomainClaims(ctx context.Context, workspaceID string) (int, error)

CountWorkspaceDomainClaims returns how many claims (pending and verified) one workspace holds across all its apps — the per-workspace custom-domain quota check (BEX_MAX_CUSTOM_DOMAINS_PER_WORKSPACE, codex-security round 18), mirroring CountGitConnections.

func (*PGStore) CountWorkspaceGroupings

func (s *PGStore) CountWorkspaceGroupings(ctx context.Context, tenantID string) (int, int, error)

CountWorkspaceGroupings implements the pool-backed count (also used outside a transaction, e.g. by reads).

func (*PGStore) CountWorkspacesForSubjectPlan

func (s *PGStore) CountWorkspacesForSubjectPlan(ctx context.Context, subject, plan string) (int, error)

CountWorkspacesForSubjectPlan counts how many workspaces of a given plan the subject owns — the per-user plan cap check (Render's five-Hobby-workspace limit). Membership, not just ownership, is the unit Render caps on.

func (*PGStore) CreateAgentSession

func (s *PGStore) CreateAgentSession(ctx context.Context, in AgentSession) (AgentSession, error)

CreateAgentSession mints the only valid agent-session id kind and persists the row in creating phase before any external OpenFGA/OpenSandbox mutation.

func (*PGStore) CreateApp

func (s *PGStore) CreateApp(ctx context.Context, a App) (App, error)

CreateApp inserts the app row and opens its first deploy row (trigger "create") in one transaction — every store-managed app has exactly one deploy the instant it exists, so ListDeploys is never truthfully empty for an app the reconciler is about to project (t001: "creating a service records deploy #1"). Bundling a second, related insert into the row's own creation is the same precedent CreateTenantWithMember already sets (a tenant plus its owner membership, one transaction) — an invariant the type itself guarantees is safer here than trusting every future caller to remember a second call.

The slug (the globally-unique public subdomain, w4/m19 t002) starts as the bare name; a apps_slug_idx collision (some other tenant already claimed that bare name) makes this retry with a random "-xxxx" suffix, matching Render's own onrender.com behavior. A tenant_id+name collision (the caller's own workspace already has this name) is a real conflict and is never retried — it classifies straight to ErrConflict.

func (*PGStore) CreateDeploy

func (s *PGStore) CreateDeploy(ctx context.Context, appID, trigger, image string, generation int64, commit CommitInfo, triggeredBy string) (Deploy, error)

func (*PGStore) CreateDisk

func (s *PGStore) CreateDisk(ctx context.Context, tenantID, appID, name, mountPath string, sizeGB int32) (Disk, error)

CreateDisk inserts the disk and opens its first size period in one transaction, so a disk can never exist unmetered or be metered before it exists.

func (*PGStore) CreateDomain

func (s *PGStore) CreateDomain(ctx context.Context, appID, host string, primary bool) (Domain, error)

func (*PGStore) CreateEnvironment

func (s *PGStore) CreateEnvironment(ctx context.Context, projectID, tenantID, name string) (Environment, error)

CreateEnvironment inserts a new environment seeded with the allow-all inbound rule pair (w4/m28): empty means deny-all now, so a fresh environment must start explicitly open (Render's dashboard seeds 0.0.0.0/0 the same way). CreateWithACL's explicit list — including an explicit deny-all [] — overwrites the seed via SetEnvironmentACL.

func (*PGStore) CreateGitHubClaimSelection

func (s *PGStore) CreateGitHubClaimSelection(ctx context.Context, sel GitHubClaimSelection) error

CreateGitHubClaimSelection records a proved candidate set. Expired rows are pruned here rather than by a janitor, matching the connect transaction: the flow is human-driven and rare, so the piggybacked DELETE keeps the table at "selections offered in the last few minutes" for free.

func (*PGStore) CreateGitHubConnectTransaction

func (s *PGStore) CreateGitHubConnectTransaction(ctx context.Context, t GitHubConnectTransaction) error

CreateGitHubConnectTransaction records a connect attempt. Expired rows are pruned here rather than by a janitor: the flow is human-driven and rare, so the piggybacked DELETE keeps the table at "attempts started in the last few minutes" for free.

func (*PGStore) CreateInvite

func (s *PGStore) CreateInvite(ctx context.Context, tenantID, email, role, token, invitedBy string, expiresAt time.Time) (Invite, error)

CreateInvite records a pending invite (ErrConflict when an outstanding invite already targets the same (tenant, email) — the partial unique index). The id is minted here; expiresAt is computed by the caller so the TTL is one policy in the service layer. Only sha256(token) is stored (w1/041); the returned Invite carries the plaintext so the caller can email the link.

func (*PGStore) CreateProject

func (s *PGStore) CreateProject(ctx context.Context, tenantID, name string) (Project, error)

func (*PGStore) CreateRegistryCredential

func (s *PGStore) CreateRegistryCredential(ctx context.Context, workspaceID, name, host, username, createdBy string, expiresAt *time.Time) (RegistryCredential, error)

CreateRegistryCredential mints a new credential row (id.New(id.RegistryCredential)) and inserts it. The caller writes the secret to OpenBao itself — this method only ever touches metadata. An empty name defaults to host.

func (*PGStore) CreateRollbackDeploy

func (s *PGStore) CreateRollbackDeploy(ctx context.Context, appID, image, rollbackOf string, generation int64, commit CommitInfo, triggeredBy string) (Deploy, error)

CreateRollbackDeploy opens a "rollback"-triggered deploy row (w2/m10): unlike a normal trigger, the restored image is already fully known (it ran live before), so resolved_image is set immediately rather than waiting for the reconciler's write-back to backfill it on convergence. commit is the TARGET deploy's commit metadata (w9/001) — a rollback re-runs what the restored deploy built, so its provenance is copied, never re-resolved against a branch that has since moved.

func (*PGStore) CreateSSHKey

func (s *PGStore) CreateSSHKey(ctx context.Context, key SSHKey) (SSHKey, error)

CreateSSHKey persists a canonicalized public key. The fingerprint's unique constraint is the ambiguity guard: one SSH handshake must map to one subject.

func (*PGStore) CreateTenant

func (s *PGStore) CreateTenant(ctx context.Context, name, plan string) (Tenant, error)

func (*PGStore) CreateTenantWithMember

func (s *PGStore) CreateTenantWithMember(ctx context.Context, identityID, plan string) (Tenant, error)

CreateTenantWithMember mints a personal tenant for an identity in one transaction. The INSERT ... ON CONFLICT (owner_identity_id) DO UPDATE is the race-safe idempotent gate: a concurrent first login that already inserted a tenant for this identity makes this a no-op that returns the winner's row (DO UPDATE exists only to surface RETURNING for the existing row). The membership upsert is idempotent for the same reason. The tenant name is its id — a unique DNS-safe placeholder.

func (*PGStore) CreateWebhookEndpoint

func (s *PGStore) CreateWebhookEndpoint(ctx context.Context, tenantID, name, url, secret string, eventTypes []string, enabled bool, createdBy string) (WebhookEndpoint, error)

CreateWebhookEndpoint mints a new endpoint row (id.New(id.Webhook)) and inserts it. The secret is minted by the caller (internal/webhooks owns the format) and returned on this one response only. Name is already validated by the service and is stored verbatim.

The count check and the insert share ONE transaction, and the count takes a row-level lock over the workspace's existing endpoints, so two concurrent creates at the boundary cannot both observe "one slot left" and both take it.

func (*PGStore) CreateWorkspace

func (s *PGStore) CreateWorkspace(ctx context.Context, name, plan, ownerSubject string) (Tenant, error)

CreateWorkspace inserts a tenant row and the owner's `admin` membership in one transaction — the atomic create the workspaces feature writes through, so a workspace never exists without an owner (the OpenFGA grant is a separate, caller-driven step, see workspaces.Service.Create). Name collisions and the plan CHECK surface as ErrConflict / ErrInvalid via classify.

func (*PGStore) CreateWorkspaceCreationAttempt

func (s *PGStore) CreateWorkspaceCreationAttempt(ctx context.Context, subject, name, plan, billingEmail string, paymentRequired bool, expiresAt time.Time) (WorkspaceCreationAttempt, error)

CreateWorkspaceCreationAttempt reserves ids but creates no tenant. The attempt is deliberately subject-bound and short-lived.

func (*PGStore) CurrentUsageCoverage

func (s *PGStore) CurrentUsageCoverage(ctx context.Context, workspaceID string, now time.Time) (UsageCoverage, error)

CurrentUsageCoverage aggregates only explicit source-health evidence. It intentionally cannot infer health from legacy usage rows or monthly totals.

func (*PGStore) DeferAgentDispatchCleanup

func (s *PGStore) DeferAgentDispatchCleanup(ctx context.Context, d AgentDispatch, next time.Time) error

func (*PGStore) DeferPreviousSandboxTeardown

func (s *PGStore) DeferPreviousSandboxTeardown(ctx context.Context, sessionID string, turn int, next time.Time) error

DeferPreviousSandboxTeardown pushes next_check_at on a previous-sandbox teardown intent after a failed terminate, without requiring abandoned=true (active steers keep the intent live while the new turn provisions).

func (*PGStore) DeleteAgentSession

func (s *PGStore) DeleteAgentSession(ctx context.Context, id string) error

DeleteAgentSession removes a session row; its transcript parts cascade with it. Two callers: the create-path compensation (a row whose OpenFGA parent tuple could not be established) and the ADR065 D4 public delete verb — the service layer owns the D4 guardrails (fresh authorization, no-live-sandbox phase gate, snapshot-blob-before-row ordering). Cancel still preserves the audit/history row.

func (*PGStore) DeleteApp

func (s *PGStore) DeleteApp(ctx context.Context, id string) error

func (*PGStore) DeleteDisk

func (s *PGStore) DeleteDisk(ctx context.Context, id string) error

DeleteDisk soft-deletes and closes the open size period in one transaction: the instant billing stops is the same instant the volume is released.

func (*PGStore) DeleteDomain

func (s *PGStore) DeleteDomain(ctx context.Context, appID, host string) error

func (*PGStore) DeleteEnvGroup

func (s *PGStore) DeleteEnvGroup(ctx context.Context, id string) (EnvGroup, error)

DeleteEnvGroup removes the row and returns it as it was, so the caller can detach its links and remove its Secrets and OpenBao maps.

func (*PGStore) DeleteEnvironment

func (s *PGStore) DeleteEnvironment(ctx context.Context, id string) error

func (*PGStore) DeleteGitConnection

func (s *PGStore) DeleteGitConnection(ctx context.Context, workspaceID string, installationID int64) error

DeleteGitConnection removes one connection of a workspace by installation id. Not-found (wrong workspace or unknown installation) is ErrNotFound. Scoping the delete to workspaceID keeps one workspace from disconnecting another's installation even if it learns the id.

func (*PGStore) DeleteInvite

func (s *PGStore) DeleteInvite(ctx context.Context, tenantID, id string) error

DeleteInvite revokes a pending invite (ErrNotFound when absent / another workspace's). Idempotency is the caller's concern; a missing row is a 404.

func (*PGStore) DeleteProject

func (s *PGStore) DeleteProject(ctx context.Context, id string) error

func (*PGStore) DeleteRegistryCredential

func (s *PGStore) DeleteRegistryCredential(ctx context.Context, workspaceID, id string) error

DeleteRegistryCredential removes the row, scoped to workspaceID. Not-found (including a cross-workspace id) is ErrNotFound.

func (*PGStore) DeleteSSHKey

func (s *PGStore) DeleteSSHKey(ctx context.Context, subject, id string) error

DeleteSSHKey scopes deletion by subject so even a guessed id cannot revoke another identity's access. Missing and foreign ids deliberately look alike.

func (*PGStore) DeleteTenant

func (s *PGStore) DeleteTenant(ctx context.Context, id string) error

DeleteTenant removes a workspace row. The FK cascades (ON DELETE CASCADE) drop its apps, their domains, and its tenant_members in the same statement; the projector then prunes the orphaned App CRs on its next pass. Returns ErrNotFound when the id doesn't exist.

func (*PGStore) DeleteUnboundBillingProviderMapping

func (s *PGStore) DeleteUnboundBillingProviderMapping(ctx context.Context, workspaceID, customerID string) error

DeleteUnboundBillingProviderMapping drops a reclaimed workspace's mapping so the next Checkout starts clean. The predicate re-checks the binding: a card bound between claim and delete keeps its row.

func (*PGStore) DeleteWebhookEndpoint

func (s *PGStore) DeleteWebhookEndpoint(ctx context.Context, tenantID, id string) error

DeleteWebhookEndpoint removes an endpoint (its deliveries cascade).

func (*PGStore) DeployIDByGeneration

func (s *PGStore) DeployIDByGeneration(ctx context.Context, appID string, generation int64) (string, error)

func (*PGStore) DevicePushSubscriptionWorkspaces

func (s *PGStore) DevicePushSubscriptionWorkspaces(ctx context.Context, subject, deviceID string) ([]string, error)

DevicePushSubscriptionWorkspaces is the by-id routing read for unregister (w5/m115): the workspaces where subject's own deviceID is registered and active. Keyed by the caller's subject, so it never reads another member's devices.

func (*PGStore) DisconnectBlueprint

func (s *PGStore) DisconnectBlueprint(ctx context.Context, id, tenantID string) error

DisconnectBlueprint marks the blueprint disconnected (hidden from list and ordinary by-ID reads; resources remain untouched). Already-disconnected and foreign IDs report ErrNotFound (w8/m37 t001 uniform absence). While a fresh apply still owns the active claim the verb refuses with ErrBlueprintSyncBusy so the caller can retry after it settles; a stale claim is settled inline as interrupted and the disconnect proceeds, which bounds the busy window after process loss without a separate recovery tick (w8/m37 t003).

func (*PGStore) DiskUsageForWindow

func (s *PGStore) DiskUsageForWindow(ctx context.Context, from, to time.Time) ([]DiskUsageRow, error)

DiskUsageForWindow returns provisioned disk GB-seconds per service for the half-open window [from, to).

It integrates the size periods rather than sampling a current size, so a disk grown mid-window contributes its old size up to the change and its new size after it, and a disk deleted mid-window contributes only up to the deletion. Because it reads committed control-plane rows, the same window recomputed later yields the same number — which is what lets the invoice be re-derived, and what makes the meter independent of the app cluster.

func (*PGStore) DomainHostsClaimed

func (s *PGStore) DomainHostsClaimed(ctx context.Context, hosts []string) ([]string, error)

DomainHostsClaimed returns which of hosts some service already claims, verified or still pending verification — exactly the hosts the unique domains.host constraint refuses a new service (w5/m116).

func (*PGStore) EndSSHSession

func (s *PGStore) EndSSHSession(ctx context.Context, sessionID, result string, endedAt time.Time) error

func (*PGStore) EnqueueBlueprintAutoSyncIntent

func (s *PGStore) EnqueueBlueprintAutoSyncIntent(ctx context.Context, intent BlueprintAutoSyncIntent) (bool, error)

EnqueueBlueprintAutoSyncIntent inserts a pending intent. ON CONFLICT (delivery_digest, blueprint_id) DO NOTHING — duplicate delivery is one logical intent. Returns whether a new row was inserted.

func (*PGStore) EnqueuePushNotifications

func (s *PGStore) EnqueuePushNotifications(ctx context.Context, items []PushNotificationBatchItem, at time.Time, key string) error

EnqueuePushNotifications inserts logical and per-device rows and advances the distinct push cursor in one transaction. Composite unique constraints make feed replay, restart, and concurrent replicas converge without duplicates.

func (*PGStore) EnqueueWebhookDeliveries

func (s *PGStore) EnqueueWebhookDeliveries(ctx context.Context, deliveries []WebhookDelivery, at time.Time, key string, maxPerWorkspace int) (WebhookEnqueueResult, error)

EnqueueWebhookDeliveries inserts a batch of pending deliveries and advances the watermark to (at, key) in one transaction — so a crash between the two can't drop events (re-reading the same rows re-enqueues them) or skip them. Called with an empty batch to advance the watermark past events nobody subscribes to. maxPerWorkspace == 0 explicitly disables the ceiling.

Every workspace represented in the batch is locked in sorted order with a transaction-scoped advisory lock before its open count is read. Concurrent dispatch replicas therefore cannot both observe the same last slot, while unrelated workspaces remain independent. Rows rejected by the ceiling are intentionally NOT errors: the watermark advances in the same transaction so notification pressure can never roll back or replay the source mutation.

func (*PGStore) EnsureBillingEnforcement

func (s *PGStore) EnsureBillingEnforcement(ctx context.Context, e BillingEnforcement) (BillingEnforcement, error)

func (*PGStore) EnsureBillingLifecycle

func (s *PGStore) EnsureBillingLifecycle(ctx context.Context, workspaceID string) (BillingLifecycle, error)

EnsureBillingLifecycle materializes the default/readable state when a Customer/Subscription is first observed. Exclusion and comp flags win.

func (*PGStore) EnsurePushWatermark

func (s *PGStore) EnsurePushWatermark(ctx context.Context, at time.Time) (time.Time, string, error)

EnsurePushWatermark seeds push's independent feed cursor on first start.

func (*PGStore) EnsureWebhookWatermark

func (s *PGStore) EnsureWebhookWatermark(ctx context.Context, at time.Time) (time.Time, string, error)

EnsureWebhookWatermark seeds the dispatcher's watermark at `at` if none exists yet and returns the current one. Seeding at first start (rather than zero) is what keeps the feature from replaying every event from before it existed.

func (*PGStore) EnvGroupNameTaken

func (s *PGStore) EnvGroupNameTaken(ctx context.Context, tenantID, name string) (bool, error)

EnvGroupNameTaken reports whether a group in the workspace uses name. It lets a create fail before its side effects; the unique constraint still decides a race.

func (*PGStore) ExpireHibernatedAgentSession

func (s *PGStore) ExpireHibernatedAgentSession(ctx context.Context, id, snapshotRef string) (AgentSession, error)

ExpireHibernatedAgentSession finalizes a retention-expired session after its snapshot has been deleted from object storage (ADR059 D5): it clears the snapshot fields and moves the row to the terminal `canceled` state with a reason, keeping the conversation history. It also stamps archived_at (ADR065 D5): a session whose snapshot aged out is one nobody came back for, so it leaves the working set by itself. Guarded to the exact snapshot_ref so it can't clobber a row a concurrent Resume already rehydrated.

func (*PGStore) ExpireWorkspaceCreationAttempts

func (s *PGStore) ExpireWorkspaceCreationAttempts(ctx context.Context, limit int) ([]WorkspaceCreationAttempt, error)

ExpireWorkspaceCreationAttempts claims a bounded cleanup batch and returns provider correlations to the trusted cleanup loop. Finalized work is never selected and can therefore never have its Customer removed.

func (*PGStore) ExtendBillingGrace

func (s *PGStore) ExtendBillingGrace(ctx context.Context, workspaceID string, extension time.Duration, actor, reason string, at time.Time) (BillingLifecycle, error)

func (*PGStore) FailAccountDeletion

func (s *PGStore) FailAccountDeletion(ctx context.Context, subject, message string) error

func (*PGStore) FailAdmittedSync

func (s *PGStore) FailAdmittedSync(ctx context.Context, id, tenantID, runID string, generation int64, completedAt time.Time, errMsg *string) error

FailAdmittedSync settles an admitted run in error without projecting Blueprint status (w8/m37 t005): a preflight or stage failure leaves current name/path/autoSync settings and status untouched — the sync never owned an apply — while releasing the active claim. The run settles only from running under the admitted generation, and the claim clears only while still owned, so a disconnect or newer admission that already took authority is never overwritten. Zero settled rows (already settled elsewhere) report ErrBlueprintSyncBusy.

func (*PGStore) FailBillingLifecycleWork

func (s *PGStore) FailBillingLifecycleWork(ctx context.Context, workspaceID string, expectedVersion int64, message string, now, retryAt time.Time) error

func (*PGStore) FailBillingNotification

func (s *PGStore) FailBillingNotification(ctx context.Context, workspaceID string, version int64, message string, next time.Time) error

func (*PGStore) FailBlueprintAutoSyncIntent

func (s *PGStore) FailBlueprintAutoSyncIntent(ctx context.Context, id, errMsg string) error

FailBlueprintAutoSyncIntent marks a claimed intent terminal-failed with a sanitized reason retained for diagnosis (never credentials / bodies).

func (*PGStore) FailRoleReconciliation

func (s *PGStore) FailRoleReconciliation(ctx context.Context, tenantID, subject, role, message string) error

func (*PGStore) FinalizeAgentSession

func (s *PGStore) FinalizeAgentSession(ctx context.Context, id, phase, headSHA, prURL string, prNumber int, evidence json.RawMessage, failureReason string) (AgentSession, TerminalTurnFact, error)

FinalizeAgentSession records a terminal turn outcome. On success it stores the pushed head SHA, the opened draft PR, and the bounded evidence extract; on failure it stores a named reason. Evidence is left untouched when nil so a failure does not erase a prior successful turn's evidence. The phase guard is the cross-replica CAS: two Completers may observe the same running row, but only one can own its terminal transition. `hibernating` is admitted solely so the snapshot reaper can unclaim its own transient phase.

The returned TerminalTurnFact carries the current turn's durable timing for metrics (w5/m88). Callers must observe only when err == nil.

func (*PGStore) FinalizeWorkspaceCreation

func (s *PGStore) FinalizeWorkspaceCreation(ctx context.Context, id, subject string, boundAt time.Time) (Tenant, error)

FinalizeWorkspaceCreation serializes finalizers on the attempt and inserts the tenant, owner membership, provider mapping, payment marker, and healthy lifecycle in one transaction. Replays return the already-created tenant.

func (*PGStore) FinishWorkspaceCreationCleanup

func (s *PGStore) FinishWorkspaceCreationCleanup(ctx context.Context, id string, success bool) error

FinishWorkspaceCreationCleanup acknowledges provider cleanup. A failure only leaves a retry delay, so a persistent provider error cannot monopolize each ordered batch; success is terminal.

func (*PGStore) ForceBillingRecovery

func (s *PGStore) ForceBillingRecovery(ctx context.Context, workspaceID, actor, reason string, at time.Time) (BillingLifecycle, error)

func (*PGStore) GetAgentSession

func (s *PGStore) GetAgentSession(ctx context.Context, id string) (AgentSession, error)

func (*PGStore) GetApp

func (s *PGStore) GetApp(ctx context.Context, id string) (App, error)

func (*PGStore) GetAppPlacements

func (s *PGStore) GetAppPlacements(ctx context.Context, appIDs []string) (map[string]AppPlacement, error)

GetAppPlacements reads only the requested immutable IDs. Missing rows are absent from the result; callers authorize the resources before requesting them.

func (*PGStore) GetAppProtectedStatus

func (s *PGStore) GetAppProtectedStatus(ctx context.Context, appID string) (string, error)

GetAppProtectedStatus resolves a store-managed App's protectedStatus via its Environment (w6/m19, apps.Service's destructive-verb guard, apps/protection.go): "unprotected" when the App has no environment_id, or its environment's own protected_status column otherwise. "unprotected" is the same literal 0024_environment_acl.up.sql defaults protected_status to — an App outside any environment behaves exactly like one in a freshly created, unprotected environment.

func (*PGStore) GetBillingLifecycle

func (s *PGStore) GetBillingLifecycle(ctx context.Context, workspaceID string) (BillingLifecycle, error)

func (*PGStore) GetBlueprint

func (s *PGStore) GetBlueprint(ctx context.Context, id, tenantID string) (Blueprint, error)

GetBlueprint fetches an active blueprint by id, scoped to tenantID (returns ErrNotFound if the id belongs to a different workspace — or names a disconnected row, which ordinary reads treat as absent, w8/m37 t001).

func (*PGStore) GetBlueprintByRepo

func (s *PGStore) GetBlueprintByRepo(ctx context.Context, tenantID, repo, branch string) (Blueprint, error)

GetBlueprintByRepo fetches the active blueprint for a tenant+repo+branch, used by the push-webhook auto-sync path. Returns ErrNotFound when unregistered — or when the row is disconnected, so webhooks cannot resurrect one (w8/m37).

func (*PGStore) GetBlueprintResourceOwner

func (s *PGStore) GetBlueprintResourceOwner(ctx context.Context, tenantID string, kind BlueprintClaimKind, name string) (string, error)

GetBlueprintResourceOwner returns the owning blueprint id, or "" if unclaimed.

func (*PGStore) GetDeploy

func (s *PGStore) GetDeploy(ctx context.Context, appID, deployID string) (Deploy, error)

func (*PGStore) GetDisk

func (s *PGStore) GetDisk(ctx context.Context, id string) (Disk, error)

func (*PGStore) GetDomainClaim

func (s *PGStore) GetDomainClaim(ctx context.Context, appID, host string) (Domain, error)

GetDomainClaim returns one exact host claim owned by appID.

func (*PGStore) GetEnvGroup

func (s *PGStore) GetEnvGroup(ctx context.Context, id string) (EnvGroup, error)

GetEnvGroup returns one group, or ErrNotFound.

func (*PGStore) GetEnvGroupByName

func (s *PGStore) GetEnvGroupByName(ctx context.Context, tenantID, name string) (EnvGroup, error)

GetEnvGroupByName returns the workspace's group named name, or ErrNotFound.

func (*PGStore) GetEnvironment

func (s *PGStore) GetEnvironment(ctx context.Context, id string) (Environment, error)

func (*PGStore) GetEnvironmentProtectedStatus

func (s *PGStore) GetEnvironmentProtectedStatus(ctx context.Context, environmentID string) (string, error)

GetEnvironmentProtectedStatus returns an Environment's protectedStatus. Missing, NULL, and empty values are all unprotected: protection is opt-in, and a resource may race with deletion of its Environment row.

func (*PGStore) GetGitConnection

func (s *PGStore) GetGitConnection(ctx context.Context, workspaceID string) (GitConnection, error)

GetGitConnection returns a workspace's oldest connection, or ErrNotFound. Since ADR075 a workspace may hold several; this backs the singular GET/POST/DELETE /v1/git/connection compatibility aliases, which act on the sole (or, ambiguously, the first) connection. Prefer ListGitConnections / GetGitConnectionByOwner for the multi-connection paths.

func (*PGStore) GetGitConnectionByOwner

func (s *PGStore) GetGitConnectionByOwner(ctx context.Context, workspaceID, accountLogin string) (GitConnection, error)

GetGitConnectionByOwner resolves the workspace connection whose GitHub account login matches accountLogin (case-insensitive), or ErrNotFound. Within a workspace account_login is unique by construction (GitHub allows one installation of a given App per account), so this is the exact connection to mint a token from for a repo owned by that account (ADR075 §4).

func (*PGStore) GetGitHubClaimSelection

func (s *PGStore) GetGitHubClaimSelection(ctx context.Context, id string) (GitHubClaimSelection, error)

GetGitHubClaimSelection reads an unexpired selection WITHOUT consuming it — the picker's render. ErrNotFound covers unknown, already-consumed, and expired alike, so a caller cannot distinguish them (and neither can an attacker probing). Subject matching is the service's job, on the fresh caller identity.

func (*PGStore) GetInvite

func (s *PGStore) GetInvite(ctx context.Context, tenantID, id string) (Invite, error)

GetInvite reads one invite scoped to its workspace (ErrNotFound otherwise) — the revoke verb's existence check so it can't delete another workspace's row.

func (*PGStore) GetInviteByToken

func (s *PGStore) GetInviteByToken(ctx context.Context, token string) (Invite, error)

GetInviteByToken reads invitation context without redeeming the capability. Accepted rows remain readable so existing members can reopen their workspace.

func (*PGStore) GetJob

func (s *PGStore) GetJob(ctx context.Context, serviceName, tenantID, jobID string) (Job, error)

func (*PGStore) GetNotificationSettings

func (s *PGStore) GetNotificationSettings(ctx context.Context, tenantID, subject string) (NotificationSettings, error)

GetNotificationSettings returns a member's EXPLICIT preference row — ErrNotFound when they never customized it (the caller applies the default).

func (*PGStore) GetProject

func (s *PGStore) GetProject(ctx context.Context, id string) (Project, error)

func (*PGStore) GetRegistryCredential

func (s *PGStore) GetRegistryCredential(ctx context.Context, workspaceID, id string) (RegistryCredential, error)

GetRegistryCredential returns one credential, scoped to workspaceID so a caller can never fetch another workspace's row by guessing its id.

func (*PGStore) GetRegistryCredentialByHost

func (s *PGStore) GetRegistryCredentialByHost(ctx context.Context, workspaceID, host string) (RegistryCredential, error)

GetRegistryCredentialByHost returns the workspace's credential for host, if any — the lookup the operator-wiring materialization path (w2/m14/t002) uses to decide whether an App's image needs a pull secret. Newest wins when a workspace somehow has more than one credential for the same host.

func (*PGStore) GetRegistryCredentialByID

func (s *PGStore) GetRegistryCredentialByID(ctx context.Context, id string) (RegistryCredential, error)

GetRegistryCredentialByID returns one credential without pre-scoping it to a workspace. It exists for resource-binding resolvers that must distinguish an unknown id (404) from an existing credential owned by another workspace (403). Public credential reads continue to use GetRegistryCredential so this lookup cannot accidentally become an unscoped read surface.

func (*PGStore) GetRegistryCredentialsByIDs

func (s *PGStore) GetRegistryCredentialsByIDs(ctx context.Context, workspaceID string, ids []string) ([]RegistryCredential, error)

GetRegistryCredentialsByIDs returns the credentials matching any id in ids that belong to workspaceID. Batch display enrichment (name resolution) only: the workspace filter keeps the lookup within the caller's tenant so a foreign id can never resolve even to its non-secret name. Unknown or foreign ids are silently omitted.

func (*PGStore) GetServiceEvent

func (s *PGStore) GetServiceEvent(ctx context.Context, workspaceID, eventID string) (ServiceEventLookup, error)

GetServiceEvent returns one event only when the owner workspace and public id both match. A foreign id and an absent id therefore share ErrNotFound, and no query hashes or scans historical source rows at request time.

func (*PGStore) GetTenant

func (s *PGStore) GetTenant(ctx context.Context, id string) (Tenant, error)

GetTenant reads one tenant by id (ErrNotFound when absent).

func (*PGStore) GetTenantMember

func (s *PGStore) GetTenantMember(ctx context.Context, tenantID, subject string) (TenantMember, error)

GetTenantMember reads one membership row (ErrNotFound when the subject is not a member) — the read the role/remove verbs consult to learn the current role (the last-admin guard) before mutating.

func (*PGStore) GetWebhookEndpoint

func (s *PGStore) GetWebhookEndpoint(ctx context.Context, tenantID, id string) (WebhookEndpoint, error)

GetWebhookEndpoint returns one endpoint (secret omitted), scoped to tenantID so a caller can never fetch another workspace's row by guessing its id.

func (*PGStore) GetWorkspaceCreationAttempt

func (s *PGStore) GetWorkspaceCreationAttempt(ctx context.Context, id, subject string) (WorkspaceCreationAttempt, error)

GetWorkspaceCreationAttempt returns not-found for an unknown OR foreign id, keeping attempt existence and provider state undiscoverable across subjects.

func (*PGStore) GitConnectionsByInstallation

func (s *PGStore) GitConnectionsByInstallation(ctx context.Context, installationID int64) ([]GitConnection, error)

GitConnectionsByInstallation returns every workspace binding for installationID, oldest first (an empty slice when none — not ErrNotFound).

It is the reverse lookup, and the ONLY direction in which an installation resolves to workspaces: every other consumer goes workspace -> connections. The push webhook reads it to confine an app-signed delivery to the proved binding set (ADR078 §4a), where an EMPTY result must mean "act on nothing" — never a widening to a global match.

func (*PGStore) HasPendingRoleReconciliation

func (s *PGStore) HasPendingRoleReconciliation(ctx context.Context, tenantID, subject string) (bool, error)

HasPendingRoleReconciliation reports whether subject still has an unconverged role intent for tenantID (round-19 #3). Not filtered on next_attempt_at: a row backed off after failures is still pending convergence, and that is exactly the window the caller needs to know about.

func (*PGStore) HibernateAgentSession

func (s *PGStore) HibernateAgentSession(ctx context.Context, id, snapshotRef string, snapshotBytes int64, snapshotSHA string, retainUntil time.Time) (AgentSession, error)

HibernateAgentSession records a durable snapshot and moves the session to the `hibernated` state (ADR059 D3): it stores the object-storage ref + size + digest, stamps hibernated_at, and sets the retention deadline (NULL for a pinned row). It deliberately KEEPS sandbox_id so the Completer can terminate the (still-live) pod after this durable write; the Completer clears it once the pod is gone (ClearAgentSessionSandbox). Guarded to the `hibernating` claim so a stale writer can't hibernate a resurrected session.

func (*PGStore) IdempotentCLIRefresh

func (s *PGStore) IdempotentCLIRefresh(
	ctx context.Context,
	tokenHash [sha256.Size]byte,
	ttl time.Duration,
	mint func(context.Context) ([]byte, int, error),
) ([]byte, int, error)

IdempotentCLIRefresh serializes one refresh-token digest across every API replica. The transaction-scoped advisory lock is acquired before the cache lookup and held until the mint marker has been persisted, so concurrent callers never mint simultaneously. Non-2xx OAuth responses and transport failures are returned but never cached.

Split storage (codex-security 2026-08 F2): the DURABLE row records only that a successful mint happened for this digest (plus its expiry — the marker is what makes a duplicate re-mint safe to allow, because the 60s rotation grace window is still open); the RESPONSE BYTES stay in this process only. Before this, Hydra's verbatim token response — access token + rotated refresh token, i.e. live credentials carrying bex.read/write/sensitive — was persisted as plaintext bytea, readable by anyone with database or backup access. A duplicate on another replica re-mints (one extra Hydra round trip inside the grace window); a duplicate on this replica within the TTL gets the exact cached bytes, preserving the byte-identical-response contract for the CLI.

func (*PGStore) InsertBlueprintSync

func (s *PGStore) InsertBlueprintSync(ctx context.Context, run BlueprintSync) (BlueprintSync, error)

InsertBlueprintSync inserts a new sync run row. Terminal fields travel with the insert so a pre-admission failure lands directly in its final error state — one statement, no stranded running row (w8/m37 t002 failSync).

func (*PGStore) InsertCLITelemetryEvent

func (s *PGStore) InsertCLITelemetryEvent(ctx context.Context, ev CLITelemetryEvent) error

func (*PGStore) InsertEnvGroup

func (s *PGStore) InsertEnvGroup(ctx context.Context, g EnvGroup) (EnvGroup, error)

InsertEnvGroup creates a group row, idle at g.Content.Generation (1 when unset), and returns it.

func (*PGStore) InsertServiceEventFact

func (s *PGStore) InsertServiceEventFact(ctx context.Context, fact ServiceEventFact) (bool, error)

InsertServiceEventFact appends a fact exactly once. A producer retry with the same SourceKey is a successful no-op.

func (*PGStore) InsertServiceEventFacts

func (s *PGStore) InsertServiceEventFacts(ctx context.Context, facts []ServiceEventFact) error

InsertServiceEventFacts appends a producer's bounded observation set in one database round trip. Each fact keeps InsertServiceEventFact's independent source-key idempotency; validation completes before anything is queued.

func (*PGStore) IsMember

func (s *PGStore) IsMember(ctx context.Context, subject, tenantID string) (bool, error)

IsMember reports whether the subject belongs to the workspace — the membership gate for an explicitly named workspace (w6/m14). A plain existence check, so it answers false (not ErrNotFound) for a non-member and for a workspace id that does not exist at all: both are "you may not act there".

func (*PGStore) LastDatastoreHealthyTransitionAt

func (s *PGStore) LastDatastoreHealthyTransitionAt(ctx context.Context, datastoreID string) (time.Time, error)

LastDatastoreHealthyTransitionAt returns the Ready=True transition time recorded with the datastore's CURRENT healthy checkpoint — the reference the reconciler's stale-conclusion guard orders an unhealthy edge against. Zero when there is no healthy checkpoint right now or its transition time is unknown; like the App path, the guard must then fail open toward recording real outages, never toward silence.

func (*PGStore) LastHealthyTransitionAt

func (s *PGStore) LastHealthyTransitionAt(ctx context.Context, appID string) (time.Time, error)

LastHealthyTransitionAt returns the Ready=True transition time recorded with the service's CURRENT healthy checkpoint — the reference the reconciler's stale-conclusion guard (w6/m41, rejectStaleUnhealthy) orders an unhealthy edge against. Zero when there is no healthy checkpoint right now or its transition time is unknown (pre-migration row, timestamp-less condition): the guard cannot order and must fail open toward recording real outages, never toward silence.

func (*PGStore) LatestDeployCommit

func (s *PGStore) LatestDeployCommit(ctx context.Context, appID string) (CommitInfo, error)

LatestDeployCommit returns the newest non-empty commit for appID. Rows with an empty commit (image-backed creates, unresolved git) are skipped so a later empty config_change does not hide an earlier repo-backed release.

func (*PGStore) LatestUsageWindow

func (s *PGStore) LatestUsageWindow(ctx context.Context, resourceKind, serviceID, kind string) (time.Time, error)

LatestUsageWindow returns the most recent window_start for one resource and meter kind, or zero time if none exist. Tracking each meter independently lets a newly-added or temporarily unavailable meter catch up without being hidden by a newer row for another meter.

func (*PGStore) LatestUsageWindowForKind

func (s *PGStore) LatestUsageWindowForKind(ctx context.Context, kind string) (time.Time, bool, error)

LatestUsageWindowForKind is the newest window any resource has recorded for one meter kind — the cursor for a meter whose rows are computed for every resource at once rather than resource by resource.

func (*PGStore) ListAbandonedBlueprintSyncs

func (s *PGStore) ListAbandonedBlueprintSyncs(ctx context.Context, before time.Time, limit int) ([]AbandonedBlueprintSync, error)

ListAbandonedBlueprintSyncs returns stale running runs for the recovery sweep, oldest first, bounded per tick (w8/m37 t004). A run qualifies only past BlueprintRunRecoveryBound — a demonstrably live run is never listed.

func (*PGStore) ListActiveBillingEnforcements

func (s *PGStore) ListActiveBillingEnforcements(ctx context.Context, workspaceID string) ([]BillingEnforcement, error)

func (*PGStore) ListActivePushSubscriptions

func (s *PGStore) ListActivePushSubscriptions(ctx context.Context) ([]ActivePushSubscription, error)

ListActivePushSubscriptions is the worker-only destination read. Membership role is read in the same query so later policy integration cannot use stale caller-supplied authorization input.

func (*PGStore) ListAgentDispatchesDue

func (s *PGStore) ListAgentDispatchesDue(ctx context.Context, now time.Time) ([]AgentDispatch, error)

ListAgentDispatchesDue bounds each recovery tick. next_check_at rotates clean tombstones out of the hot set without forgetting a possibly late sandbox.

func (*PGStore) ListAgentSessions

func (s *PGStore) ListAgentSessions(ctx context.Context, workspaceID string, q AgentSessionListQuery) ([]AgentSession, error)

ListAgentSessions returns one filtered page of a workspace's sessions, newest first (created_at DESC, id DESC — the order the keyset cursor pages). The default query (zero filters) is the unarchived working set.

func (*PGStore) ListAgentSessionsByPhases

func (s *PGStore) ListAgentSessionsByPhases(ctx context.Context, phases []string) ([]AgentSession, error)

ListAgentSessionsByPhases returns every session across all workspaces in any of the given phases, oldest first. It powers the trusted background Completer loop (ADR047 D4) that finalizes running sessions; it performs no authorization and must never be reachable from a tenant-facing verb.

func (*PGStore) ListApps

func (s *PGStore) ListApps(ctx context.Context) ([]App, error)

func (*PGStore) ListAuditEvents

func (s *PGStore) ListAuditEvents(ctx context.Context, workspaceID string, filter AuditFilter) ([]AuditRow, error)

ListAuditEvents returns workspaceID's audit trail — newest-first by default, oldest-first with filter.OldestFirst — honoring filter's time bounds/cursor/limit — the one query the REST and GraphQL read fragments (internal/audit) both delegate to, so they can't diverge.

func (*PGStore) ListAutoSyncBlueprints

func (s *PGStore) ListAutoSyncBlueprints(ctx context.Context, branch, tenantScope string) ([]Blueprint, error)

ListAutoSyncBlueprints returns every auto-sync-enabled, non-disconnected Blueprint on branch. When tenantScope is non-empty, results are confined to that workspace (GitHub App installation binding). Repo URL matching is left to the caller (repoURLsMatch) because stored repo strings vary by form.

func (*PGStore) ListBillingExportIssues

func (s *PGStore) ListBillingExportIssues(ctx context.Context, openOnly bool, limit int) ([]BillingExportIssue, error)

func (*PGStore) ListBillingOwnerSubjects

func (s *PGStore) ListBillingOwnerSubjects(ctx context.Context, workspaceID string) ([]string, error)

ListBillingOwnerSubjects returns the PEOPLE to contact about a workspace's billing (w5/m103): admins, excluding machine bindings — an API key is not somebody who can act on a failed payment.

func (*PGStore) ListBillingProviderMappings

func (s *PGStore) ListBillingProviderMappings(ctx context.Context, livemode bool, limit int) ([]BillingProviderMapping, error)

func (*PGStore) ListBlueprintResourceClaims

func (s *PGStore) ListBlueprintResourceClaims(ctx context.Context, tenantID, blueprintID string) ([]BlueprintResourceClaim, error)

ListBlueprintResourceClaims returns claims owned by blueprintID.

func (*PGStore) ListBlueprintSyncs

func (s *PGStore) ListBlueprintSyncs(ctx context.Context, blueprintID, cursor string, limit int) ([]BlueprintSync, error)

ListBlueprintSyncs returns sync runs for a blueprint, newest first, with cursor-based paging (exclusive, by started_at DESC + id).

func (*PGStore) ListBlueprints

func (s *PGStore) ListBlueprints(ctx context.Context, tenantID string) ([]Blueprint, error)

ListBlueprints returns all non-disconnected blueprints for a tenant, newest first.

func (*PGStore) ListDeploys

func (s *PGStore) ListDeploys(ctx context.Context, appID string, filter DeployFilter) ([]Deploy, error)

func (*PGStore) ListDesiredApps

func (s *PGStore) ListDesiredApps(ctx context.Context) ([]DesiredApp, error)

func (*PGStore) ListDisks

func (s *PGStore) ListDisks(ctx context.Context, tenantID, appID string) ([]Disk, error)

func (*PGStore) ListDomainClaims

func (s *PGStore) ListDomainClaims(ctx context.Context, appID string) ([]Domain, error)

ListDomainClaims returns pending and verified claims in stable projection order, primary first and then creation order.

func (*PGStore) ListEnvGroups

func (s *PGStore) ListEnvGroups(ctx context.Context, tenantID string) ([]EnvGroup, error)

ListEnvGroups returns one workspace's groups in id order.

func (*PGStore) ListEnvironmentServices

func (s *PGStore) ListEnvironmentServices(ctx context.Context, environmentID, projectID string) ([]string, error)

ListEnvironmentServices returns the public ids of all services currently in the environment. Filters on project_id too (not just environment_id): a service's project_id can drift out from under it via the independent setProjectServices verb (which knows nothing about environments), so this defends against surfacing a service that no longer belongs to the environment's own project.

func (*PGStore) ListEnvironments

func (s *PGStore) ListEnvironments(ctx context.Context, projectID string) ([]Environment, error)

func (*PGStore) ListEnvironmentsForProjects

func (s *PGStore) ListEnvironmentsForProjects(ctx context.Context, projectIDs []string) ([]Environment, error)

ListEnvironmentsForProjects returns the Environments of every project in projectIDs in one query, each project's ordered as ListEnvironments orders it. A page of projects reads its environments together (w5/146).

func (*PGStore) ListGitConnections

func (s *PGStore) ListGitConnections(ctx context.Context, workspaceID string) ([]GitConnection, error)

ListGitConnections returns all of a workspace's connections, oldest first (an empty slice when it has none — not ErrNotFound). This is the aggregate the multi-account repo picker and the connections surface read (ADR075).

func (*PGStore) ListHibernatedForRetention

func (s *PGStore) ListHibernatedForRetention(ctx context.Context, now time.Time, limit int) ([]AgentSession, error)

ListHibernatedForRetention returns unpinned hibernated sessions whose retention deadline has passed (ADR059 D5), oldest deadline first — the working set the retention sweep reclaims. The sweep deletes ONLY the snapshot object; the row and its transcript are kept (ExpireHibernatedAgentSession moves the row to canceled + archived). Pinned rows (retain_until NULL) never match. Trusted background read; the partial index backs it.

func (*PGStore) ListInvites

func (s *PGStore) ListInvites(ctx context.Context, tenantID string) ([]Invite, error)

ListInvites returns a workspace's OUTSTANDING invites — not yet accepted and not expired — oldest first. Accepted/expired rows are audit history, not pending work, so the Team page's pending list excludes them.

func (*PGStore) ListJobs

func (s *PGStore) ListJobs(ctx context.Context, serviceName, tenantID string, filter JobListFilter) ([]Job, error)

func (*PGStore) ListNotifyRecipients

func (s *PGStore) ListNotifyRecipients(ctx context.Context, tenantID string) ([]NotifyRecipient, error)

ListNotifyRecipients returns every PERSON in tenantID (w5/m103 — an API key's binding has no mailbox; before the kind filter, every deploy failure tried to notify bound client ids) with their resolved deploy-notification preferences: an explicit row's values via the LEFT JOIN, or the failure-only default via COALESCE for a member who never customized them. One query serves the notification fan-out on every deploy lifecycle event, rather than a settings lookup per member.

func (*PGStore) ListOpenDeploys

func (s *PGStore) ListOpenDeploys(ctx context.Context) ([]Deploy, error)

func (*PGStore) ListOwnDevicePushSubscriptions

func (s *PGStore) ListOwnDevicePushSubscriptions(ctx context.Context, tenantID, subject string) ([]DevicePushSubscription, error)

ListOwnDevicePushSubscriptions returns active devices for one exact member. The SQL projection intentionally omits token and digest so even an accidental direct JSON encoding of the result cannot disclose the bearer capability.

func (*PGStore) ListOwnPushNotifications

func (s *PGStore) ListOwnPushNotifications(ctx context.Context, tenantID, subject string, limit int, excludeEvents []string) ([]PushNotification, error)

ListOwnPushNotifications pages the caller's durable inbox. excludeEvents removes destination-gated event types (notifications.destination_policy.go) in SQL, shared with CountUnreadPushNotifications.

func (*PGStore) ListOwnWebPushSubscriptions

func (s *PGStore) ListOwnWebPushSubscriptions(ctx context.Context, tenantID, subject string) ([]WebPushSubscription, error)

func (*PGStore) ListPreviousSandboxTeardownsDue

func (s *PGStore) ListPreviousSandboxTeardownsDue(ctx context.Context, now time.Time) ([]AgentDispatch, error)

ListPreviousSandboxTeardownsDue returns dispatch intents that still name a previous sandbox to tear down, due for a retry. Bound turns keep the intent as an abandoned tombstone until this cleanup succeeds (w5/m99 t004).

func (*PGStore) ListProjects

func (s *PGStore) ListProjects(ctx context.Context, tenantID string) ([]Project, error)

func (*PGStore) ListRegistryCredentials

func (s *PGStore) ListRegistryCredentials(ctx context.Context, workspaceID string) ([]RegistryCredential, error)

ListRegistryCredentials returns a workspace's stored credentials, newest first.

func (*PGStore) ListRetiredUsageResources

func (s *PGStore) ListRetiredUsageResources(ctx context.Context, since time.Time) ([]RetiredUsageResource, error)

ListRetiredUsageResources returns the resources retired at or after since whose workspace still exists.

func (*PGStore) ListSSHKeys

func (s *PGStore) ListSSHKeys(ctx context.Context, subject string) ([]SSHKey, error)

ListSSHKeys returns only one identity's keys in stable creation order.

func (*PGStore) ListSandboxTenantKeys

func (s *PGStore) ListSandboxTenantKeys(ctx context.Context) ([]SandboxTenantKey, error)

ListSandboxTenantKeys lets the internal poller enumerate tenant-scoped OpenSandbox views without weakening the public workspace isolation seam.

func (*PGStore) ListServiceEvents

func (s *PGStore) ListServiceEvents(ctx context.Context, appID, ownerWorkspace string, f ServiceEventFilter) ([]ServiceEventRow, error)

ListServiceEvents returns one service's composed activity feed, newest first. Audit, deploy, and fact rows all use the immutable control-plane app id.

func (*PGStore) ListServicesForProjects

func (s *PGStore) ListServicesForProjects(ctx context.Context, projectIDs []string) (map[string][]string, error)

ListServicesForProjects returns the public ids of the services in each of projectIDs, keyed by project id and ordered by name, in one query for the whole page (w5/155). A project with no services has no key.

func (*PGStore) ListTenantMembers

func (s *PGStore) ListTenantMembers(ctx context.Context, tenantID string) ([]TenantMember, error)

ListTenantMembers returns a workspace's PEOPLE, oldest first (w5/m103): machine bindings are excluded, because every consumer of this read — the Team page, the Render owners members API, seat usage — is answering "who belongs to this workspace", and an API key is not a who. The key's own row still exists and still authorizes; it is listed on the API-keys surface instead.

func (*PGStore) ListTenants

func (s *PGStore) ListTenants(ctx context.Context) ([]Tenant, error)

func (*PGStore) ListTenantsForSubject

func (s *PGStore) ListTenantsForSubject(ctx context.Context, subject string) ([]Tenant, error)

ListTenantsForSubject returns the workspaces the subject belongs to, oldest first — the caller's workspace list (the dashboard switcher / owners list).

func (*PGStore) ListTerminalAgentSessionsForPush

func (s *PGStore) ListTerminalAgentSessionsForPush(ctx context.Context, since time.Time) ([]AgentSession, error)

ListTerminalAgentSessionsForPush returns completed/failed sessions updated at or after `since`, across all workspaces, for the push worker's agent-terminal projection (w11/m6 t005). Bounded by the recency window so the scan stays cheap; dedup to one push per (session, phase) is the notification's source_event_key ON CONFLICT, not this query. Like ListAgentSessionsByPhases it performs no authorization and is a trusted background read only.

func (*PGStore) ListTerminalAgentSessionsWithSandbox

func (s *PGStore) ListTerminalAgentSessionsWithSandbox(ctx context.Context, now time.Time) ([]AgentSession, error)

ListTerminalAgentSessionsWithSandbox returns terminal sessions (completed/ failed/canceled) that still carry a sandbox id and are due for a reap attempt (sandbox_reap_after IS NULL or <= now), oldest first (ADR054 D6 / w5/m99). Eligibility is per-row — there is no now-relative updated_at window, so a teardown that fails for days is still retried once its backoff elapses. Trusted background read — no authorization. `now` drives the backoff gate.

func (*PGStore) ListWebhookAttempts

func (s *PGStore) ListWebhookAttempts(ctx context.Context, filter WebhookAttemptFilter) ([]WebhookAttempt, error)

ListWebhookAttempts returns one endpoint's completed network exchanges, newest first, keyset-paged on immutable (sent_at,id). Unsent reservations do not pretend to have exchange evidence; QueueWebhookResend returns its pending reservation directly and it joins history after the worker terminalizes it.

func (*PGStore) ListWebhookDispatchEndpoints

func (s *PGStore) ListWebhookDispatchEndpoints(ctx context.Context, since time.Time) ([]WebhookEndpoint, error)

ListWebhookDispatchEndpoints returns the dispatcher's subscription table, refreshed each poll pass: every endpoint that is enabled or whose last interval ended after `since`, with ALL its retained intervals in Eligibility. Every interval, because the feed cursor follows recording time while eligibility is judged on source time: a fact recorded late can belong to an interval the cursor passed long ago. A disabled endpoint stays while its enabled-period events may still arrive — they are owed to it, and the send pass holds them until it is enabled again — so the caller passes the feed position less its late-recording allowance.

Only the columns dispatch needs are selected. The destination URL is loaded later, per due delivery, so a large catalog cannot pin unbounded URL text in the shared API process (codex round-15 #2).

func (*PGStore) ListWebhookEndpoints

func (s *PGStore) ListWebhookEndpoints(ctx context.Context, tenantIDs []string, afterAt time.Time, afterKey string, limit int) ([]WebhookEndpoint, error)

ListWebhookEndpoints returns the requested workspaces' endpoints, newest first, keyset-paged on immutable (created_at,id); secrets are omitted.

func (*PGStore) ListWebhookEvents

func (s *PGStore) ListWebhookEvents(ctx context.Context, afterAt time.Time, afterKey string, until time.Time, verbs, tenants []string, limit int) ([]WebhookEventRow, error)

ListWebhookEvents returns the composed feed strictly after the watermark (afterAt, afterKey), up to and including `until`, oldest first, restricted to `tenants` (the subscriber set). verbs is the audit-verb subset the webhook vocabulary maps (internal/webhooks owns the mapping, exactly as internal/events owns its own).

func (*PGStore) ListWorkspaceEnvironmentServices

func (s *PGStore) ListWorkspaceEnvironmentServices(ctx context.Context, tenantID string) (map[string][]string, error)

ListWorkspaceEnvironmentServices batches the service-membership half of the workspace scope index. Joining environments preserves the same project-id consistency check as ListEnvironmentServices, so stale app rows are omitted.

func (*PGStore) ListWorkspaceEnvironments

func (s *PGStore) ListWorkspaceEnvironments(ctx context.Context, tenantID string) ([]Environment, error)

ListWorkspaceEnvironments returns every Environment in one workspace. The workspace scope index needs this shape directly; making the store express it avoids issuing one ListEnvironments query per Project.

func (*PGStore) MaintainGitWebhookReplayEpochs

func (s *PGStore) MaintainGitWebhookReplayEpochs(ctx context.Context, epochs []GitWebhookReplayEpoch, now time.Time) (int64, error)

MaintainGitWebhookReplayEpochs heartbeats this process's accepted epochs, expires dead replica leases, and purges claims only for epochs no live lease still accepts. The finite pre-migration legacy partition is never purged: it has no authenticated epoch to prove retired.

func (*PGStore) MarkBillingEnforcementRecovered

func (s *PGStore) MarkBillingEnforcementRecovered(ctx context.Context, workspaceID, kind, name string, at time.Time) error

func (*PGStore) MarkCheckoutStarted

func (s *PGStore) MarkCheckoutStarted(ctx context.Context, workspaceID string, at time.Time) error

MarkCheckoutStarted records that a hosted Checkout session was created for the workspace. This is intent, NOT payment state. Monotonic like the bind marker — the first attempt is the one worth keeping — and silent for an absent mapping: a workspace without a Customer opens Checkout without one (Stripe creates it on submit), so only workspaces that already own a Customer, and rows minted back when Checkout pre-created it, carry the stamp.

func (*PGStore) MarkMachineMemberships

func (s *PGStore) MarkMachineMemberships(ctx context.Context, clientIDs []string) (int64, error)

MarkMachineMemberships reclassifies the tenant_members rows of known API-key client ids as kind='machine' — the one-time reconcile for bindings written before w5/m103 added the column (they defaulted to 'user', since Hydra is the only registry that knows which subjects are clients and SQL cannot consult it). bex-api runs this at startup against the live client list, the same shape as the deploy-hook digest backfill.

It is idempotent, and deliberately one-directional: it never demotes a row back to 'user'. A client id that no longer exists in Hydra is not evidence that a human appeared under the same subject — it is a revoked key whose binding UnbindClient should have removed.

Returns how many rows it changed, so a startup that fixes nothing is distinguishable from one that quietly did work.

func (*PGStore) MarkOwnPushNotificationRead

func (s *PGStore) MarkOwnPushNotificationRead(ctx context.Context, tenantID, subject, eventID string, at time.Time) (bool, error)

func (*PGStore) MarkUsageAttempted

func (s *PGStore) MarkUsageAttempted(ctx context.Context, attempts []UsageExportAttempt, at time.Time) error

MarkUsageAttempted durably records the deterministic identifier before any provider call. The first-attempt timestamp never moves: once the identifier window expires, the row is quarantined instead of being replayed blindly.

func (*PGStore) MarkUsageEmitted

func (s *PGStore) MarkUsageEmitted(ctx context.Context, rows []HourlyRow, at time.Time) error

MarkUsageEmitted stamps emitted_at on exactly the given rows — the outbox write after a successful ingest. Keyed by the full primary key (resource_kind, service_id, kind, tier, window_start), it only touches rows still un-emitted (emitted_at IS NULL), so a re-run after a crash between ingest and stamp is a safe no-op. A single unnest-driven statement keeps the whole batch atomic.

func (*PGStore) MarkWorkspaceCreationSetupSucceeded

func (s *PGStore) MarkWorkspaceCreationSetupSucceeded(ctx context.Context, id, subject, paymentMethodID string) (WorkspaceCreationAttempt, error)

func (*PGStore) OAuthRevokedAt

func (s *PGStore) OAuthRevokedAt(ctx context.Context, subject, clientID string) (time.Time, bool, error)

OAuthRevokedAt returns the latest shared invalidation marker for one OAuth consent chain. Missing rows are the normal never-revoked state.

func (*PGStore) ObserveSandboxMeter

func (s *PGStore) ObserveSandboxMeter(ctx context.Context, obs SandboxMeterObservation) error

ObserveSandboxMeter atomically advances one sandbox cursor and accrues the non-overlapping interval since its previous observation. Only an interval whose previous phase was running is charged; creating/resuming/suspended/ errored/terminated intervals are zero. Replaying or racing an older sample is a no-op, which is the restart/retry no-double-count guarantee.

func (*PGStore) OwnerIDForSubject

func (s *PGStore) OwnerIDForSubject(ctx context.Context, subject string) (string, error)

OwnerIDForSubject returns the stable opaque "own-" id for a subject, minting and persisting one on first sight (w6/m7): OwnerIDsForSubjects for one subject. The Render owners members surface reports userId as this id instead of leaking the raw Kratos/Hydra subject.

func (*PGStore) OwnerIDsForSubjects

func (s *PGStore) OwnerIDsForSubjects(ctx context.Context, subjects []string) (map[string]string, error)

OwnerIDsForSubjects returns each subject's opaque own- id, minting one on first sight. The ids a list already has are read in one query, and only a subject without one is written (w5/104): the Team page polls its members every 30s, and an upsert writes a row version even when its DO UPDATE changes nothing. A miss still goes through the upsert, so a concurrent first sight of the same subject yields exactly one row (the subject is the primary key), and RETURNING surfaces the stored id rather than the freshly minted candidate.

func (*PGStore) PaymentEligibility

func (s *PGStore) PaymentEligibility(ctx context.Context, workspaceID string) (PaymentEligibility, error)

PaymentEligibility reads the marker and both billing exemptions in one local SELECT. The tenant row is authoritative, so a workspace that has not entered Checkout yet still returns an unstamped (not missing) snapshot.

func (*PGStore) PaymentMethodBound

func (s *PGStore) PaymentMethodBound(ctx context.Context, workspaceID string) (bool, error)

PaymentMethodBound is the narrow marker accessor. An unknown or unstamped workspace returns false rather than leaking whether a tenant exists.

func (*PGStore) Ping

func (s *PGStore) Ping(ctx context.Context) error

Ping reports whether the database is reachable — the /healthz check.

func (*PGStore) PreviewAccountDeletion

func (s *PGStore) PreviewAccountDeletion(ctx context.Context, subject string, machineSubjects []string) ([]AccountWorkspaceDisposition, error)

func (*PGStore) PromoteDomainClaim

func (s *PGStore) PromoteDomainClaim(ctx context.Context, appID, id, expectedChallenge string, at time.Time) (Domain, error)

PromoteDomainClaim atomically promotes the same pending row that supplied expectedChallenge. A concurrent delete/recreate cannot promote the new row. Already-verified rows are idempotent only when the id still matches.

func (*PGStore) PruneRetiredUsageResources

func (s *PGStore) PruneRetiredUsageResources(ctx context.Context, before time.Time) (int64, error)

PruneRetiredUsageResources drops records retired before before — their hours are past the meters' catch-up window — and records whose workspace no longer exists, which has no usage left to write.

func (*PGStore) PruneWebhookEligibility

func (s *PGStore) PruneWebhookEligibility(ctx context.Context, before time.Time) (int64, error)

PruneWebhookEligibility deletes closed eligibility intervals that ended before `before` — the delivery retention horizon. A feed read lagging past it would treat those events as ineligible (skip, never replay), the same side the dispatcher errs on everywhere else.

func (*PGStore) PurgeAuditEvents

func (s *PGStore) PurgeAuditEvents(ctx context.Context, before time.Time) (int64, error)

PurgeAuditEvents deletes rows older than before — the retention sweep (internal/audit, BEX_AUDIT_RETENTION_DAYS) calls this, mirroring CompactUsage's atomic, idempotent delete.

func (*PGStore) PurgeCLITelemetryEvents

func (s *PGStore) PurgeCLITelemetryEvents(ctx context.Context, before time.Time) (int64, error)

PurgeCLITelemetryEvents applies the platform retention window to ingested CLI telemetry: stable installation UUIDs must not become an indefinitely retained trail. It runs in the audit service's scheduled sweep beside PurgeAuditEvents/PurgeSSHSessions (w5/m92).

func (*PGStore) PurgeProductAnalytics

func (s *PGStore) PurgeProductAnalytics(ctx context.Context, before time.Time) (int64, error)

PurgeProductAnalytics shares the audit retention window, not its row counts.

func (*PGStore) PurgeSSHSessions

func (s *PGStore) PurgeSSHSessions(ctx context.Context, before time.Time) (int64, error)

PurgeSSHSessions applies the platform audit-retention window to SSH session metadata as well as ordinary audit_events. Remote addresses and target ids must not become an indefinitely retained second audit trail.

func (*PGStore) PurgeStripeBillingEvents

func (s *PGStore) PurgeStripeBillingEvents(ctx context.Context, before time.Time) (int64, error)

func (*PGStore) PushDeliveryStats

func (s *PGStore) PushDeliveryStats(ctx context.Context) (PushQueueStats, error)

func (*PGStore) PushNotificationWorkspaces

func (s *PGStore) PushNotificationWorkspaces(ctx context.Context, subject, eventID string) ([]string, error)

PushNotificationWorkspaces returns the workspaces in which subject's own inbox holds eventID, ordered by tenant id; none for an unknown id. A routing read only (w4/m172): mark-read with no ownerId acts in the item's own workspace, then still authorizes there and updates through the tenant-scoped MarkOwnPushNotificationRead. Keyed by subject, so it can never surface another member's row; the tenant_members FK means every returned workspace is one the subject belongs to. Rows are unique per (tenant, subject, event_id), not per (subject, event_id), hence a list.

func (*PGStore) QuarantineOldUsageAttempts

func (s *PGStore) QuarantineOldUsageAttempts(ctx context.Context, before, at time.Time) (int64, error)

QuarantineOldUsageAttempts stops automatic replay after Stripe's rolling identifier de-duplication window. These rows require explicit reconciliation and an audited operator decision.

func (*PGStore) QueueWebhookResend

func (s *PGStore) QueueWebhookResend(ctx context.Context, request WebhookResendRequest) (WebhookAttempt, error)

QueueWebhookResend reserves one immediate manual attempt for a source attempt, owner-scoped through its endpoint. The caller key is durable: a repeat returns the same pending or completed attempt forever. A scheduled, unsent automatic retry is parked on the manual row and restored only when that manual exchange fails, so Resend neither consumes nor races the normal retry budget.

func (*PGStore) ReclaimEmptyBlueprintGroupings

func (s *PGStore) ReclaimEmptyBlueprintGroupings(ctx context.Context, tenantID string, pairs []GroupingPair, referencedEnvironments, referencedProjects map[string]bool) (removedEnvironments, removedProjects []string, err error)

ReclaimEmptyBlueprintGroupings deletes, in ONE transaction, the named grouping rows a disconnected Blueprint minted that nothing still references (w8/m20 t004): an environment goes when no apps row is assigned to it and its id is not in referencedEnvironments (CR-side datastore members); a candidate project goes when it has no environments left, no apps members, and is not in referencedProjects. Deployed resources are never touched — a populated grouping survives (Render disconnect semantics). Returns the removed names for post-commit auditing.

func (*PGStore) ReconcileUsageSourceStreams

func (s *PGStore) ReconcileUsageSourceStreams(ctx context.Context, active []UsageResourceRef, through time.Time) error

ReconcileUsageSourceStreams closes open streams whose resource has left the collector inventory. The observed health rows are retained for the rest of the month; only the stream's expected range becomes finite.

func (*PGStore) Record

func (s *PGStore) Record(ctx context.Context, ev core.AuditEvent) error

Record persists one audit event — *PGStore structurally satisfies core.AuditSink (Record(ctx, core.AuditEvent) error), so the composition root wires it directly onto core.Base.Audit (cmd/api/main.go) with no adapter type needed.

func (*PGStore) RecordAgentSessionDispatch

func (s *PGStore) RecordAgentSessionDispatch(ctx context.Context, id, sandboxID, phase, status, deliveryMode string, turn int) (AgentSession, error)

RecordAgentSessionDispatch binds a newly dispatched accepted turn: it advances the phase/status, adopts a fresh sandbox id when re-dispatched, records the delivery mode ("resume" or "redispatch"). Prompt acceptance already advanced the turn counter transactionally.

Binding and abandonment serialize on the session and dispatch intent. Only the accepted turn in its provisioning phase can bind; cancellation, recovery, and newer turns fence out stale workers. Successful binding removes the intent in the same transaction so recovery cannot mistake its sandbox for an orphan.

func (*PGStore) RecordBillingNotificationMailed

func (s *PGStore) RecordBillingNotificationMailed(ctx context.Context, workspaceID string, version int64, subject string) error

RecordBillingNotificationMailed adds subject to the owners the notice reached. Recording a subject twice keeps one entry.

func (*PGStore) RecordDomainVerificationAttempt

func (s *PGStore) RecordDomainVerificationAttempt(ctx context.Context, appID, id string, at time.Time) error

RecordDomainVerificationAttempt records a conservative failed-or-successful resolver attempt without changing ownership state.

func (*PGStore) RecordObservedDatastoreState

func (s *PGStore) RecordObservedDatastoreState(ctx context.Context, obs ObservedDatastoreState) ([]DatastoreEventFact, error)

RecordObservedDatastoreState atomically advances a datastore checkpoint and appends any availability edge it crossed — the Database/KeyValue twin of RecordObservedServiceState. The first observation is a baseline; replaying the same observation is a no-op.

func (*PGStore) RecordObservedServiceState

func (s *PGStore) RecordObservedServiceState(ctx context.Context, obs ObservedServiceState) ([]ServiceEventFact, error)

RecordObservedServiceState atomically advances a service checkpoint and appends any phase/availability edges it crossed. The first observation is a baseline; replaying the same observation is a no-op.

func (*PGStore) RecordProductActivity

func (s *PGStore) RecordProductActivity(ctx context.Context, e core.ProductActivity) error

RecordProductActivity enriches the durable, deduplicated successful effect. Subject locking prevents an in-flight recorder restoring identity after deletion.

An unset surface falls back to the column's default rather than being sent as an empty string, which its CHECK would reject. This is defensive only: the sole caller reaches here through ObserveProductActivity, which clamps the value, and logs-and-swallows any error — so no value written here can fail a resource creation. It does not make arbitrary input safe; a direct caller passing "Dashboard" still violates the CHECK, and should.

func (*PGStore) RecordProductDomainTLS

func (s *PGStore) RecordProductDomainTLS(ctx context.Context, domain string, ready bool, at time.Time) error

func (*PGStore) RecordProductHosting

func (s *PGStore) RecordProductHosting(ctx context.Context, workspace, resource, kind string, live bool, at time.Time) (bool, error)

RecordProductHosting samples current state at most every five minutes unless it changes. was_live is monotonic within the UTC day; live is the latest sample.

func (*PGStore) RecordPushReceipt

func (s *PGStore) RecordPushReceipt(ctx context.Context, d DuePushDelivery, code string, at, next time.Time, delivered, failed, ambiguous bool) (bool, error)

func (*PGStore) RecordPushSendFailure

func (s *PGStore) RecordPushSendFailure(ctx context.Context, delivery DuePushDelivery, code string, at, next time.Time, terminal bool) (bool, error)

func (*PGStore) RecordResourceDisplayNames

func (s *PGStore) RecordResourceDisplayNames(ctx context.Context, tenantID string, records []ResourceDisplayName) error

RecordResourceDisplayNames upserts a batch of retained names for one tenant. It is called at service create and rename, before datastore collection, and during usage reads to refresh known names. Blank kinds, ids and names are skipped, so a resource bex never knew leaves no misleading name record.

Upsert, not insert: a rename must move the retained name forward, which is what makes a live renamed resource bill under its current name.

func (*PGStore) RecordRetiredUsageResource

func (s *PGStore) RecordRetiredUsageResource(ctx context.Context, r RetiredUsageResource) error

RecordRetiredUsageResource stores r, retired now. Retiring the same resource again (a delete retried after one that failed) replaces the record.

func (*PGStore) RecordStripeBillingEvent

func (s *PGStore) RecordStripeBillingEvent(ctx context.Context, e StripeBillingEvent, grace time.Duration) (BillingLifecycle, bool, bool, error)

RecordStripeBillingEvent inserts the provider event and applies its normalized transition in one transaction. Duplicate ids and stale provider timestamps are retained/recognized without repeating a state change or notification.

func (*PGStore) RecordUsageExportResult

func (s *PGStore) RecordUsageExportResult(ctx context.Context, accepted []UsageExportAttempt, rejected []UsageExportReject, at time.Time) error

RecordUsageExportResult atomically stamps accepted rows and moves permanent rejects into the durable issue table. Transient failures are intentionally absent: they remain pending with their immutable first-attempt timestamp.

func (*PGStore) RecordUsageSourceHealth

func (s *PGStore) RecordUsageSourceHealth(ctx context.Context, records []UsageSourceRecord) error

RecordUsageSourceHealth records an explicit degraded/unavailable attempt when no usage row can be committed. A later retry overwrites the state for the same source-window and reopens a stream closed by inventory reconciliation.

func (*PGStore) RefreshInvite

func (s *PGStore) RefreshInvite(ctx context.Context, tenantID, id, token string, expiresAt time.Time) (Invite, error)

RefreshInvite pushes an unaccepted invite's expiry forward and replaces its token — the resend verb's write half (w1/m33). The token rotates (w1/041): with only sha256(token) at rest the old plaintext cannot be re-emailed, so resend mints a fresh capability and the freshly emailed link supersedes the original, which stops redeeming. An expired-but-unaccepted invite is revived (resend is how an admin recovers a lapsed invite without churning the id); an accepted or unknown invite is ErrNotFound — accepted rows are audit history, not pending work. The returned Invite carries the plaintext token for the resent mail.

func (*PGStore) RehydrateAgentSession

func (s *PGStore) RehydrateAgentSession(ctx context.Context, id, sandboxID, phase, status, deliveryMode string) (AgentSession, error)

RehydrateAgentSession completes a rehydration (ADR059 D4): it adopts the fresh sandbox id and advances to the live phase. A Steer's turn counter already advanced atomically in BeginRehydrate; a prompt-less Resume does not advance. clears the snapshot fields (the blob has been consumed into the new pod — a re-hibernation writes a fresh one). Guarded to the `resuming` claim BeginRehydrate set, so it can't race a retention delete or a double resume.

func (*PGStore) ReleaseBlueprintResourceClaim

func (s *PGStore) ReleaseBlueprintResourceClaim(ctx context.Context, tenantID string, kind BlueprintClaimKind, name, blueprintID string) error

ReleaseBlueprintResourceClaim drops one blueprint's claim on one resource — what a sync does for a resource its manifest stopped declaring (w4/m125).

The blueprint_id predicate is the whole point: between the list and this delete another blueprint may have taken the resource over with an explicit confirmation, and a release that ignored the current owner would silently undo that takeover. A row already owned by someone else is a no-op, which is the correct outcome — this blueprint no longer manages it either way.

func (*PGStore) ReleaseBlueprintResourceClaims

func (s *PGStore) ReleaseBlueprintResourceClaims(ctx context.Context, tenantID, blueprintID string) error

ReleaseBlueprintResourceClaims drops every claim held by blueprintID in the workspace (disconnect). Resources and CR labels are cleared separately.

func (*PGStore) ReleaseEnvGroupContent

func (s *PGStore) ReleaseEnvGroupContent(ctx context.Context, id string, expectedVersion int64, rel EnvGroupRelease) error

ReleaseEnvGroupContent ends the claim held at expectedVersion.

func (*PGStore) ReleaseGitWebhookDelivery

func (s *PGStore) ReleaseGitWebhookDelivery(ctx context.Context, claim GitWebhookReplayClaim) error

ReleaseGitWebhookDelivery frees a claim whose delivery failed before completing, allowing the git host's retry to process it.

func (*PGStore) ReleasePushDelivery

func (s *PGStore) ReleasePushDelivery(ctx context.Context, delivery DuePushDelivery) (bool, error)

ReleasePushDelivery leaves a failed provider attempt retryable without persisting provider text. expectedClaim protects a newer lease.

func (*PGStore) RemoveAccountMember

func (s *PGStore) RemoveAccountMember(ctx context.Context, tenantID, subject string) error

RemoveAccountMember rechecks that another admin exists and removes the deleting subject atomically. It is intentionally narrower than general member removal and clears only the onboarding owner binding.

func (*PGStore) RemoveDomain

func (s *PGStore) RemoveDomain(ctx context.Context, appID, host string) error

RemoveDomain deletes a domain row for apps.IntentStore. If host is the canonical target of an auto-generated redirecting sibling, that sibling is deleted in the same statement: preserving it while clearing its redirect would silently turn a generated redirect into a directly served claim. Deleting the redirecting sibling itself leaves the canonical host unchanged. A sibling the tenant explicitly re-added has a NULL redirect_for_name and is therefore independent. The operation is idempotent (not-found is ignored).

func (*PGStore) RemoveMember

func (s *PGStore) RemoveMember(ctx context.Context, tenantID, subject string) error

RemoveMember deletes a membership row (ErrNotFound when absent). The FGA tuple removal is the caller's separate, best-effort step (members.Service.Remove).

SECURITY (codex round-16 #3): removing an admin re-counts under the same tenant advisory lock as UpdateMemberRole so concurrent remove/demote cannot erase the last administrator.

func (*PGStore) RenameEnvironment

func (s *PGStore) RenameEnvironment(ctx context.Context, id, name string) error

func (*PGStore) RenameProject

func (s *PGStore) RenameProject(ctx context.Context, id, name string) error

func (*PGStore) RenameTenant

func (s *PGStore) RenameTenant(ctx context.Context, id, name string) (Tenant, error)

RenameTenant updates a workspace's display name (ErrConflict on collision, ErrNotFound when the id doesn't exist).

func (*PGStore) ReplaceDomainClaims

func (s *PGStore) ReplaceDomainClaims(ctx context.Context, appID string, declarations []DomainDeclaration) ([]Domain, error)

ReplaceDomainClaims atomically reconciles a declaration set while preserving unchanged rows' ownership evidence. Newly introduced hosts start pending.

func (*PGStore) ReplaceDomains

func (s *PGStore) ReplaceDomains(ctx context.Context, appID, primary string, hosts []string) error

ReplaceDomains atomically reconciles one App's complete domain set. Locking the App row serializes concurrent re-syncs of the same service; the global UNIQUE(domains.host) index arbitrates claims between different services. Any conflict rolls the transaction back, preserving the former set.

func (*PGStore) ResolveBillingExportIssue

func (s *PGStore) ResolveBillingExportIssue(ctx context.Context, transactionID, action, actor, reason string, at time.Time) (BillingExportIssue, error)

ResolveBillingExportIssue performs one explicit, audited operator decision: acknowledge keeps the row held, retry requeues only a definite permanent reject still inside Stripe's event-time horizon, and mark_repaired stamps the row after external reconciliation proves the provider outcome.

func (*PGStore) ResourceDisplayNames

func (s *PGStore) ResourceDisplayNames(ctx context.Context, tenantID string, refs []ResourceDisplayName) (map[string]string, error)

ResourceDisplayNames resolves retained names for one tenant, keyed by ResourceDisplayNameKey. Missing ids are simply absent from the map — a resource bex never recorded a name for has none to retain.

func (*PGStore) RevokeAllDevicePushSubscriptions

func (s *PGStore) RevokeAllDevicePushSubscriptions(ctx context.Context, tenantID, subject string) (int64, error)

RevokeAllDevicePushSubscriptions removes every active destination for the caller in this workspace, used by explicit all-device logout/revocation.

func (*PGStore) RevokeAllWebPushSubscriptions

func (s *PGStore) RevokeAllWebPushSubscriptions(ctx context.Context, tenantID, subject string) (int64, error)

func (*PGStore) RevokeDevicePushSubscription

func (s *PGStore) RevokeDevicePushSubscription(ctx context.Context, tenantID, subject, deviceID string) (bool, error)

RevokeDevicePushSubscription is an idempotent, member-scoped logout/delete. A guessed device id belonging to another member looks exactly like no row.

func (*PGStore) RevokeExactPushSubscription

func (s *PGStore) RevokeExactPushSubscription(ctx context.Context, d DuePushDelivery) (bool, error)

func (*PGStore) RevokeWebPushSubscription

func (s *PGStore) RevokeWebPushSubscription(ctx context.Context, tenantID, subject, browserID string) (bool, error)

func (*PGStore) RollbackAppCreation

func (s *PGStore) RollbackAppCreation(ctx context.Context, appID string) error

RollbackAppCreation removes only a just-provisioned App and the analytics emitted while attempting it, in one transaction. Ordinary deletion preserves recent history even when its creation event has aged out of retention.

func (*PGStore) RunGitWebhookReplayMaintenance

func (s *PGStore) RunGitWebhookReplayMaintenance(ctx context.Context, epochs []GitWebhookReplayEpoch)

RunGitWebhookReplayMaintenance keeps epoch liveness authoritative across API replicas. A graceful stop removes its leases without waiting for crash expiry.

func (*PGStore) RunGroupingTx

func (s *PGStore) RunGroupingTx(ctx context.Context, fn func(GroupingStore) error) error

RunGroupingTx runs fn against a transaction-scoped GroupingStore: every grouping write inside fn commits together or not at all. fn returning an error (or a panic) rolls the whole set back.

func (*PGStore) SSHKeyByFingerprint

func (s *PGStore) SSHKeyByFingerprint(ctx context.Context, fingerprint string) (SSHKey, error)

SSHKeyByFingerprint is the gateway-only lookup that maps a presented public key to its owning subject. It returns public metadata only.

func (*PGStore) SandboxKeyForWorkspace

func (s *PGStore) SandboxKeyForWorkspace(ctx context.Context, workspaceID string) (string, error)

SandboxKeyForWorkspace returns the workspace's OpenSandbox tenant key, minting one on first use. It is the KeyProvider behind every sandbox request, so it reads a stored key first: the upsert writes a new row version even when it changes nothing (w5/087). Race-safe: first-mints that all miss the read collapse on UNIQUE(workspace_id), and DO UPDATE (where DO NOTHING would return no row on conflict) hands each loser the stored key.

func (*PGStore) SandboxKeyLookup

func (s *PGStore) SandboxKeyLookup(ctx context.Context, workspaceID string) (string, bool, error)

SandboxKeyLookup returns the workspace's existing OpenSandbox tenant key WITHOUT minting one (unlike SandboxKeyForWorkspace) — the workspace-delete purger (w1/m61) uses it to find the key that enumerates + terminates the workspace's sandboxes. found=false means no key was ever minted, which the purger reads as "no sandbox was ever created" (the first create is what mints the key), so there is nothing to tear down. It never writes, so it is safe to call during teardown without resurrecting a key the cascade is about to drop.

func (*PGStore) SandboxUsageMetadata

func (s *PGStore) SandboxUsageMetadata(ctx context.Context, tenantID string, sandboxIDs []string) (map[string]SandboxUsageMetadata, error)

SandboxUsageMetadata resolves labels for both current and previous session sandboxes, which outlive their workloads, alongside durable meter observations. A repo-less session (w3/m79) is labelled by its id, "agent session <id>": unique, and findable in the session list, where the create-time `plan · image` name would not say which session ran (w1/112).

func (*PGStore) ScheduleAgentSessionSandboxReap

func (s *PGStore) ScheduleAgentSessionSandboxReap(ctx context.Context, id string, now time.Time) error

ScheduleAgentSessionSandboxReap pushes the next deferred-teardown attempt for a terminal session whose terminate just failed. Attempts grow the backoff (1m, 2m, 4m, … capped at 1h) so a flapping substrate does not hammer the API, but the row never ages out of the scan (w5/m99 t002).

func (*PGStore) SelectUnemittedUsage

func (s *PGStore) SelectUnemittedUsage(ctx context.Context, floor, sealBefore time.Time, limit int, requirePaymentMethod bool) ([]HourlyRow, error)

SelectUnemittedUsage returns up to limit sealed usage_hourly rows that have not yet shipped to Stripe — the billing outbox read (docs/ADR040-billing-metronome.md §4). A row qualifies when its window is final (window_start < sealBefore, i.e. past the rewrite horizon), not below the billing floor (window_start >= floor), still un-emitted, and its workspace is not billing_excluded. Oldest first, so batches ship in order and the loop makes forward progress. The JOIN filters excluded tenants at the source: an excluded workspace's rows are never even considered for export.

func (*PGStore) ServiceEventWorkspaces

func (s *PGStore) ServiceEventWorkspaces(ctx context.Context, eventID string) ([]string, error)

ServiceEventWorkspaces returns every workspace that indexes an evt-… id, ordered by workspace id; none for an unknown id. A routing read only (w4/m172): the verb picks one of them, authorizes there and loads the event through the workspace-scoped GetServiceEvent. Event ids are DERIVED from the source row's event key (ids.Derive), not minted per workspace, and the key itself embeds a globally unique source id — but a workspace:default audit row is indexed under every matching owner (migration 0083), so the same id can legitimately resolve to several workspaces. The full, deterministic list is returned so the caller can prefer one it is a member of instead of the store guessing.

func (*PGStore) ServiceNameTaken

func (s *PGStore) ServiceNameTaken(ctx context.Context, tenantID, selfID, name string) (bool, error)

ServiceNameTaken reports whether another service in tenantID than selfID ("" for one not created yet) was created as, or is displayed as, name: the refusal CreateApp and SetAppDisplayName make under the workspace's name lock (refuseTakenServiceName), read ahead of them so a create's plan and a patch's preflight meet it before anything is written (w5/m116).

func (*PGStore) SetAgentSessionArchived

func (s *PGStore) SetAgentSessionArchived(ctx context.Context, id string, archived bool) (AgentSession, error)

SetAgentSessionArchived flips the ADR065 D1 archive flag. Idempotent both ways: re-archiving keeps the original archived_at (the first archive time is the honest one), and unarchiving a working-set row is a no-op. It never touches phase/status — archive is list-state, not lifecycle-state.

func (*PGStore) SetAgentSessionCanceled

func (s *PGStore) SetAgentSessionCanceled(ctx context.Context, id string) (AgentSession, TerminalTurnFact, error)

SetAgentSessionCanceled is the Cancel verb's terminal CAS (w5/m88): first writer to leave a non-canceled phase wins, stamps the current turn's completed_at, and returns timing for metrics. A repeated Cancel after the session is already canceled returns ErrNotFound so observers do not double-count.

func (*PGStore) SetAgentSessionFailure

func (s *PGStore) SetAgentSessionFailure(ctx context.Context, id, sandboxID, reason string) (AgentSession, error)

SetAgentSessionFailure terminalizes a still-active session from a background dispatch/resume/steer failure with a named reason. Unlike SetAgentSessionLifecycle it records the reason in failure_reason (the field the dashboard's failed-session callout reads) and sets status to the terminal 'failed', so a provisioning failure surfaces identically to a driver-reported failure via FinalizeAgentSession (w5/m80 t005, closes w5/048). It preserves SetAgentSessionLifecycle's conditional sandbox_id write so an egress-transition failure still records the just-canceled sandbox for the reaper. The caller (setLifecycleIfActive) guards against clobbering a settled/canceling session.

func (*PGStore) SetAgentSessionLifecycle

func (s *PGStore) SetAgentSessionLifecycle(ctx context.Context, id, sandboxID, phase, status string, canceled bool) (AgentSession, error)

SetAgentSessionLifecycle advances the durable session state. canceled=true stamps canceled_at exactly once; every transition advances updated_at.

func (*PGStore) SetAgentSessionPinned

func (s *PGStore) SetAgentSessionPinned(ctx context.Context, id string, pinned bool, retainUntil *time.Time) (AgentSession, error)

SetAgentSessionPinned toggles the never-expire pin (ADR059 D5). Pinning clears retain_until (removes the delete edge); unpinning puts a hibernated row back on the clock with the supplied deadline (retainUntil is ignored — left NULL — while the row is not hibernated). It authorizes nothing; the caller gates it.

func (*PGStore) SetAppDisplayName

func (s *PGStore) SetAppDisplayName(ctx context.Context, id string, displayName string) error

SetAppDisplayName mirrors spec.displayName onto the row. Empty means never renamed, and readers fall back to apps.name.

It also moves the retained display name forward (w2/m96 t001) so the service bills under its current name, and keeps that name after deletion. The retained value follows the same "empty means fall back to apps.name" rule, resolved here rather than left for every reader.

func (*PGStore) SetAppEnvironment

func (s *PGStore) SetAppEnvironment(ctx context.Context, id, projectID, environmentID string) error

SetAppEnvironment atomically assigns an App row to an Environment and its owning Project. Empty ids clear both memberships (Blueprint ungrouped:).

func (*PGStore) SetAppIdleTTL

func (s *PGStore) SetAppIdleTTL(ctx context.Context, id string, seconds int32) error

SetAppIdleTTL updates the row's idle-TTL seconds (the apps feature's idle-timeout verb validates the bound before calling this). The projector carries it onto spec.idleTTLSeconds the same way it carries replicas.

func (*PGStore) SetAppImage

func (s *PGStore) SetAppImage(ctx context.Context, id string, image string) error

SetAppImage updates the row's image (the deploys feature's Rollback verb, w2/m10). The projector carries it onto spec.image the same way it carries replicas/tier — the write-through-store discipline every intent field with a row as its single writer of truth follows.

func (*PGStore) SetAppPort

func (s *PGStore) SetAppPort(ctx context.Context, id string, port int32, mode string) error

SetAppPort saves both the port and whether it overrides image discovery. A projector resync must not undo a successful service-port update.

func (*PGStore) SetAppReplicas

func (s *PGStore) SetAppReplicas(ctx context.Context, id string, replicas int32) error

SetAppReplicas updates the row's replica count (the apps feature's scale verb validates the bound before calling this). The projector carries it onto spec.replicas the same way it carries suspended/tier.

func (*PGStore) SetAppSource

func (s *PGStore) SetAppSource(ctx context.Context, id, repo, image, branch string, registryCredentialID *string) error

SetAppSource atomically updates repo/image/branch and the explicit registry credential binding so the projector never observes mismatched source/auth.

func (*PGStore) SetAppSuspended

func (s *PGStore) SetAppSuspended(ctx context.Context, id string, suspended bool) error

func (*PGStore) SetAppTier

func (s *PGStore) SetAppTier(ctx context.Context, id string, tier string) error

SetAppTier updates the row's tier (the apps feature's plan-change verb validates it against lego/types/tiers before calling this). The projector carries it onto spec.tier the same way it carries suspended.

func (*PGStore) SetBillingException

func (s *PGStore) SetBillingException(ctx context.Context, workspaceID, exception string, enabled bool, actor, reason string, at time.Time) (bool, BillingLifecycle, error)

SetBillingException atomically applies/removes the structural exclusion or rated-but-free comp flag and moves any billing-owned suspension through the ordinary recovery worker. reason is a bounded operator explanation, never a payment detail or arbitrary payload.

func (*PGStore) SetBlueprintSyncNote

func (s *PGStore) SetBlueprintSyncNote(ctx context.Context, runID, note string) error

SetBlueprintSyncNote annotates an already-admitted run. The note is prose about what the run did that its state cannot say, so it is composed by the service layer and written here (w4/120: a create that revived a disconnected row is a new connection on an old row, and the sync history below it belongs to the previous one).

Admission notes can be written while the run is in flight. Detachment notices are written only after successful completion; that ordering belongs to the service, which knows whether the apply actually succeeded.

func (*PGStore) SetDeployPreDeployStatus

func (s *PGStore) SetDeployPreDeployStatus(ctx context.Context, id, status string) (bool, error)

SetDeployPreDeployStatus records the pre-deploy step's outcome on a deploy row (w1/m33), no-op when unchanged (IS DISTINCT FROM guards the write so the reconciler's every-pass projection doesn't churn the row). Returns whether a row was updated.

func (*PGStore) SetDeployStallReason

func (s *PGStore) SetDeployStallReason(ctx context.Context, id, reason string) error

SetDeployStallReason records (or clears) why an open deploy is not progressing — w4/m112's in-flight sibling of the terminal failure_reason.

Guarded three ways, because this runs on every reconcile pass over every open deploy:

  • `finished_at IS NULL` — a terminal row's story belongs to failure_reason/cancel_reason; a late pass must not reopen it.
  • `stall_reason IS DISTINCT FROM $2` — an unchanged observation writes nothing, so a stall that persists for 15 minutes costs one UPDATE.
  • `updated_at` is deliberately NOT touched. This is an observation about the row, not a transition of it; bumping the row's clock would churn every updated_at-keyed reader (the events feed, the deploy list's ordering) once per pass for as long as the stall lasts.

func (*PGStore) SetEnvironmentACL

func (s *PGStore) SetEnvironmentACL(ctx context.Context, id, protectedStatus string, networkIsolationEnabled bool, ipAllowList []core.IPAllowListEntry) error

SetEnvironmentACL replaces the full protected-environment ACL triple — full-replace, not a merge, matching every other "Set" verb in this store (SetEnvironmentServices, postgres.SetIPAllowList): the caller always supplies all three fields, never a partial patch.

func (*PGStore) SetEnvironmentServices

func (s *PGStore) SetEnvironmentServices(ctx context.Context, environmentID, projectID, tenantID string, serviceIDs []string) ([]core.ServicePlacementChange, error)

SetEnvironmentServices replaces the full list of services in an environment (within tenantID): clears any apps currently assigned to it, then assigns only the identified apps — mirroring SetProjectServices exactly. Public srv- ids are canonical; names remain accepted for backward compatibility with clients from before stable service ids shipped. It ALSO stamps project_id to projectID on the assigned apps: Render's model treats "in an environment" as "in that project," so assigning to an environment is sufficient to join its project too (a caller doesn't need two calls). Service ids/names not found in tenantID are silently skipped (the UPDATE affects 0 rows for them, the same convention SetProjectServices uses). Returns the per-service placement diff (w6/m134) the service layer records move events from — empty for a no-op replacement.

func (*PGStore) SetPaymentMethodBound

func (s *PGStore) SetPaymentMethodBound(ctx context.Context, workspaceID string, at time.Time) error

SetPaymentMethodBound stamps the marker monotonically. Replayed Stripe webhooks retain the first successful bind timestamp, so the operation is idempotent and never moves the enforcement snapshot backwards or forwards.

func (*PGStore) SetProjectServices

func (s *PGStore) SetProjectServices(ctx context.Context, projectID, tenantID string, serviceIDs []string) ([]core.ServicePlacementChange, error)

SetProjectServices replaces the full list of services in a project (within tenantID): clears any apps currently assigned to it, then assigns only the identified apps. Public srv- ids are canonical; names remain accepted for backward compatibility with clients from before stable service ids shipped. Also NULLs environment_id in the same transaction on departing rows (w4/m32) and on named rows whose environment belongs to another project (w4/m187) — a service must never keep an environment outside its project (and the App CR's frozen spec.environmentIPAllowList that implies): ListEnvironmentServices filters on project_id too, so such a row silently drops out of every environment fan-out while its k8s-projected rules stay stuck. Retained members keep a compatible environment. Returns the per-service placement diff (w6/m134): the service layer records move events from it and derives the environment-layer clear list (rows whose environment this transaction NULLed are exactly the changes with EnvironmentFrom set and EnvironmentTo nil, since nothing here ever sets environment_id). Service ids/names not found in tenantID are silently skipped (the UPDATE affects 0 rows for them).

func (*PGStore) SetTenantBillingExcluded

func (s *PGStore) SetTenantBillingExcluded(ctx context.Context, tenantID string, excluded bool, actor string, at time.Time) (bool, error)

SetTenantBillingExcluded flips a workspace's billing-exclusion flag (docs/ADR040-billing-metronome.md §7, Mode A) and, when the value actually changes, records an audit_events row (verb billing.SetExclusion) attributing it to actor. This flag decides whether money is owed, so its only caller is the admin-only control-plane internal API — never a tenant. Returns whether the value changed (a no-op toggle writes no audit row); ErrNotFound when the workspace does not exist.

func (*PGStore) SetWebhookEndpointEnabled

func (s *PGStore) SetWebhookEndpointEnabled(ctx context.Context, tenantID, id string, enabled bool, reason string) (WebhookEndpoint, error)

SetWebhookEndpointEnabled flips an endpoint's enabled flag (the caller's manual toggle — also how an auto-disabled endpoint is re-armed). Enabling clears any disabled reason; disabling records the caller's.

func (*PGStore) SetWorkspaceCreationSetup

func (s *PGStore) SetWorkspaceCreationSetup(ctx context.Context, id, subject, customerID, setupIntentID string, livemode bool) (WorkspaceCreationAttempt, error)

SetWorkspaceCreationSetup records only safe provider correlations. A retry may repeat the same values; changing an already-recorded object is refused.

func (*PGStore) SetWorkspaceCreationSubscription

func (s *PGStore) SetWorkspaceCreationSubscription(ctx context.Context, id, subject, subscriptionID string) error

func (*PGStore) SettleCanceledPreDeploy

func (s *PGStore) SettleCanceledPreDeploy(ctx context.Context, appID string, generation int64, status string, finishedAt time.Time) (bool, error)

SettleCanceledPreDeploy settles the row and its pre_deploy_ended fact in one statement, so a fact can never be lost to a row that already reads settled.

func (*PGStore) StageBlueprintManifest

func (s *PGStore) StageBlueprintManifest(ctx context.Context, id, tenantID string, generation int64, runID, manifest string) (Blueprint, error)

StageBlueprintManifest stores the admitted sync's preflighted manifest. Only the manifest (the sync-owned field) is written — current name/path/autoSync settings are retained (w8/m37 t005). Zero rows means the generation was fenced by a newer admission or a disconnect → ErrBlueprintSyncBusy.

func (*PGStore) StartSSHSession

func (s *PGStore) StartSSHSession(ctx context.Context, session SSHSessionAudit) error

func (*PGStore) SubjectIsWorkspaceAdmin

func (s *PGStore) SubjectIsWorkspaceAdmin(ctx context.Context, tenantID, subject string) (bool, error)

SubjectIsWorkspaceAdmin reports whether subject currently holds the admin role in tenantID — the webhook failure-notice recipient gate (round-14 #6): a notice discloses (a redacted projection of) a destination that may have been configured by a different admin after this subject was removed or demoted, so CURRENT authorization state, not created_by provenance, decides the recipient. False for a non-member, a non-admin, and a workspace that does not exist — all equally "do not mail".

func (*PGStore) SweepPushRetention

func (s *PGStore) SweepPushRetention(ctx context.Context, revokedBefore, terminalBefore time.Time) (PushSweepResult, error)

func (*PGStore) SweepWebhookDeliveries

func (s *PGStore) SweepWebhookDeliveries(ctx context.Context, before time.Time, keepPerEndpoint, limit int) (int64, error)

SweepWebhookDeliveries purges reclaimable delivery rows (w1/m67 F3). The table is both the durable delivery QUEUE and the product's history surface, so before m67 nothing ever reclaimed a finished row: ordinary tenant activity grew shared table, index, and backup storage without bound.

Terminal notifications (delivered_at or failed_at set, with no pending child) are purged with all attempts when either:

  • older than `before`, so history has a finite lifetime; or
  • beyond `keepPerEndpoint` most recent rows for their endpoint, so a burst inside the age window cannot evade the age rule alone.

keepPerEndpoint counts immutable attempts, not parents: a notification with eight retries consumes eight evidence slots. Whole-parent deletion prevents orphaning payloads or leaving a partial forensic sequence.

Notifications whose newest pending reservation is older than `before` are abandoned and purged too. This includes a manual resend parked on a previously terminal parent; recent reservations on old source events remain safe.

Deletion is bounded per call (`limit` for each of the two passes) and safe to run concurrently on two replicas — rows are claimed with FOR UPDATE SKIP LOCKED, so a second sweeper simply takes different rows. Returns the number deleted.

func (*PGStore) TenantContactSubjects

func (s *PGStore) TenantContactSubjects(ctx context.Context, tenantIDs []string) (map[string]string, error)

TenantContactSubjects returns, for each of tenantIDs, the subject whose email is the workspace's contact: its owner binding when set, else its oldest user admin, ties broken by subject (w5/126). One query serves a whole list. Unknown ids and workspaces with neither are absent.

func (*PGStore) TenantForIdentity

func (s *PGStore) TenantForIdentity(ctx context.Context, subject string) (Tenant, error)

TenantForIdentity resolves the subject's default workspace — see the Store interface for the contract. ORDER BY m.created_at is what makes it a *default* rather than an arbitrary row: the bare join returned whichever membership Postgres happened to yield first, so a caller with two workspaces could resolve to a different one call to call (w6/m11 hit this live). The tenant id is the tie-break for two memberships written in the same instant (the same-transaction case: a workspace create inserts tenant + membership together), so the answer is stable even then.

func (*PGStore) TenantForOwner

func (s *PGStore) TenantForOwner(ctx context.Context, identityID string) (Tenant, error)

TenantForOwner returns the personal tenant an identity OWNS (by owner_identity_id), or ErrNotFound — the cheap read the onboarding path tries before the upsert, so a returning caller's login is a single SELECT, not a write transaction. Unlike TenantForIdentity (a membership JOIN that can now return a workspace the caller was merely invited to), this returns only the workspace the caller actually owns (w4/m12).

func (*PGStore) TenantOwnerSubject

func (s *PGStore) TenantOwnerSubject(ctx context.Context, tenantID string) (string, error)

TenantOwnerSubject returns the identity a workspace's onboarding binding names (tenants.owner_identity_id), or "" when the binding is NULL — the shape every workspace minted through CreateWorkspace has, since only onboarding's CreateTenantWithMember sets it. ErrNotFound when the workspace doesn't exist.

The membership guards read it (w5/m101): the owner binding is the key EnsureTenant resolves the personal workspace by, so a removed or demoted owner is either silently re-granted admin on their next request or left with no workspace at all — both are refused instead.

func (*PGStore) TerminateMissingSandboxMeters

func (s *PGStore) TerminateMissingSandboxMeters(ctx context.Context, workspaceID string, seen []string, observedAt time.Time) error

TerminateMissingSandboxMeters closes active cursors absent from one complete tenant-scoped OpenSandbox list. The caller invokes it only after a successful list, so an upstream outage never turns running sandboxes into terminated ones. The last known running interval accrues through observedAt.

func (*PGStore) TouchBillingProviderMapping

func (s *PGStore) TouchBillingProviderMapping(ctx context.Context, workspaceID string, at time.Time) error

TouchBillingProviderMapping rotates a mapping to the back of the bounded reconciliation queue after every attempt. That prevents the oldest 500 workspaces (including a permanently failing one) from starving the rest.

func (*PGStore) TouchProject

func (s *PGStore) TouchProject(ctx context.Context, id string) error

TouchProject advances projects.updated_at. Link verbs that only re-label Database/KeyValue CRs still count as a project modification for Render's project.updatedAt (w4/m109).

func (*PGStore) TouchRegistryCredential

func (s *PGStore) TouchRegistryCredential(ctx context.Context, workspaceID, id string) error

TouchRegistryCredential bumps updated_at with no other column change — used when only the OpenBao-held secret was rotated, so the metadata row still reflects when the credential last changed.

func (*PGStore) TransitionDeploy

func (s *PGStore) TransitionDeploy(ctx context.Context, id, status, resolvedImage, failureReason, failureCode, cancelReason string, startedAt *time.Time) (bool, error)

func (*PGStore) UnbindClient

func (s *PGStore) UnbindClient(ctx context.Context, clientID string) error

UnbindClient permanently revokes an API key and removes its tenant_members rows atomically. The marker also rejects already-issued tokens on every API replica. Keys are never reused, so an existing marker remains authoritative. A key that was never bound is still revoked without returning an error.

func (*PGStore) UpdateBlueprint

func (s *PGStore) UpdateBlueprint(ctx context.Context, id, tenantID string, name *string, autoSync *bool, path *string, status *string, lastSyncAt *time.Time) (Blueprint, error)

UpdateBlueprint applies a partial update (only non-zero pointer fields are changed). Returns the updated row. Disconnected rows are untouched — ordinary updates cannot reactivate one (w8/m37 t001).

func (*PGStore) UpdateBlueprintSync

func (s *PGStore) UpdateBlueprintSync(ctx context.Context, id, state string, completedAt *time.Time, errMsg *string) (BlueprintSync, error)

UpdateBlueprintSync updates a sync run's state, completion timestamp, and error reason (nil on success, the failure's message on error).

func (*PGStore) UpdateDisk

func (s *PGStore) UpdateDisk(ctx context.Context, id string, name, mountPath *string, sizeGB *int32) (Disk, error)

UpdateDisk applies the mutable fields. A size change closes the current period and opens a new one at the same instant, so the meter integrates the old size right up to the grow and the new size from it — a disk grown mid-hour bills both, which is what makes the invoice reproducible.

func (*PGStore) UpdateEnvGroupMeta

func (s *PGStore) UpdateEnvGroupMeta(ctx context.Context, g EnvGroup) (EnvGroup, error)

UpdateEnvGroupMeta writes g's name, Environment, links and updated_at if the row still carries g.MetaVersion, and returns the new row. A moved version is ErrEnvGroupChanged, a missing row ErrNotFound, a taken name ErrEnvGroupNameTaken.

func (*PGStore) UpdateJobStatus

func (s *PGStore) UpdateJobStatus(ctx context.Context, jobID, status string) (Job, error)

UpdateJobStatus transitions a job to the given terminal or running status, stamping the appropriate timestamp. The UPDATE is a no-op on already-terminal rows (prevents double-close races) — callers check the returned row's status.

func (*PGStore) UpdateMemberRole

func (s *PGStore) UpdateMemberRole(ctx context.Context, tenantID, subject, role string) error

UpdateMemberRole changes an existing member's role (ErrNotFound when the subject is not a member of the workspace). The role CHECK is enforced by the API layer's validation, not the column, so an unknown role is a caller error mapped upstream, not a constraint violation here.

SECURITY (codex round-16 #3): demoting an admin re-counts admins inside the same transaction under a tenant advisory lock so two concurrent demotions cannot both pass a standalone count and leave zero admins.

func (*PGStore) UpdateRegistryCredential

func (s *PGStore) UpdateRegistryCredential(ctx context.Context, workspaceID, id, name, username string, expiresAt *time.Time) (RegistryCredential, error)

UpdateRegistryCredential changes name/username/expiresAt in place (a rotated secret is a separate OpenBao write the caller makes itself — this method never touches secret material). Nil expiresAt clears any existing expiry.

func (*PGStore) UpdateTenantPlan

func (s *PGStore) UpdateTenantPlan(ctx context.Context, id, plan string) (Tenant, error)

UpdateTenantPlan changes a workspace's plan (ErrNotFound when the id doesn't exist). The plan is validated by the caller (workspaces.Service.ChangePlan via store.NormalizePlan) before this write; the `tenants.plan` CHECK constraint is the last-resort backstop, surfaced as ErrInvalid via classify.

func (*PGStore) UpdateWebhookEndpoint

func (s *PGStore) UpdateWebhookEndpoint(ctx context.Context, tenantID, id, name, url string, eventTypes []string, enabled bool) (WebhookEndpoint, error)

UpdateWebhookEndpoint replaces an endpoint's mutable fields (name, url, event_types, enabled) in one SQL round trip. Re-enabling an endpoint clears any disabled_reason; disabling leaves it unchanged (use SetWebhookEndpointEnabled for an explicit reason). Secret is immutable after creation.

func (*PGStore) UpsertBillingProviderMapping

func (s *PGStore) UpsertBillingProviderMapping(ctx context.Context, m BillingProviderMapping) error

func (*PGStore) UpsertBlueprint

func (s *PGStore) UpsertBlueprint(ctx context.Context, b Blueprint) (Blueprint, error)

UpsertBlueprint creates a blueprint or refreshes its manifest when (tenant_id, repo, branch) already exists. The id field is ignored on conflict — the existing row's id is preserved. Returns the current row.

The conflict arm writes the manifest ONLY (w8/m37 t005): deploy-time auto-registration must never clobber current name/path/autoSync settings or lifecycle status with deploy-request defaults — a direct deploy racing an admitted sync (or a mid-run settings change) keeps the row's settings. It also refuses disconnected rows (re-establishment is confined to explicit creation through AdmitBlueprintCreate, which bumps the execution generation). Generation and active-run ownership are never written here — an auto-register racing a live sync must not fence its own admission.

func (*PGStore) UpsertDevicePushSubscription

func (s *PGStore) UpsertDevicePushSubscription(ctx context.Context, sub DevicePushSubscription) (DevicePushSubscription, error)

UpsertDevicePushSubscription atomically registers or replaces one app installation. The workspace lock makes both cumulative quotas race-safe; the token digest lock serializes account-switch races for the same provider capability. A token moved to another member/device revokes its old row before activation.

func (*PGStore) UpsertNotificationPushPolicy

func (s *PGStore) UpsertNotificationPushPolicy(ctx context.Context, tenantID, subject string, policy json.RawMessage) (NotificationSettings, error)

UpsertNotificationPushPolicy replaces only the caller's native-push policy. On first write it creates the same existing notification_settings row with the established failure-only email defaults; later writes leave every email preference byte-for-byte unchanged.

func (*PGStore) UpsertNotificationSettings

func (s *PGStore) UpsertNotificationSettings(ctx context.Context, tenantID, subject string, deployStarted, deploySucceeded, deployFailed bool) (NotificationSettings, error)

UpsertNotificationSettings writes a member's preferences, creating the row on first write and updating it (bumping updated_at) thereafter — the single write path both REST/GraphQL/MCP `UpdateSettings` uses.

func (*PGStore) UpsertUsageHourly

func (s *PGStore) UpsertUsageHourly(ctx context.Context, row HourlyRow) error

UpsertUsageHourly writes one window row, creating it or updating the quantity to the new value if the (resource_kind, service_id, kind, tier, window_start) key already exists. The ON CONFLICT … DO UPDATE makes the rollup loop idempotent.

func (*PGStore) UpsertWebPushSubscription

func (s *PGStore) UpsertWebPushSubscription(ctx context.Context, sub WebPushSubscription) (WebPushSubscription, error)

func (*PGStore) UsageMonthToDate

func (s *PGStore) UsageMonthToDate(ctx context.Context, workspaceID string, now time.Time) ([]UsageSummaryRow, error)

UsageMonthToDate returns month-to-date totals for all services in a workspace, grouped by (resource_kind, service_id, kind, tier). now is the caller-supplied clock value so tests can drive it without real time. "Month to date" is calendar-month-start (UTC) to now.

The query sums usage_hourly and usage_monthly together (w8/m4): a hot month has only hourly rows, a compacted month has only its monthly aggregate, and a month caught mid-transition (partially compacted at the 48 h clamp) has both — the UNION ALL sum is exact in every state, so period queries never depend on whether compaction has run yet.

func (*PGStore) WebhookEndpointWorkspace

func (s *PGStore) WebhookEndpointWorkspace(ctx context.Context, id string) (string, error)

WebhookEndpointWorkspace returns the workspace that owns an endpoint id, or ErrNotFound. A routing read only: the verb still authorizes against that workspace and loads the row through the tenant-scoped reads (w4/m172).

func (*PGStore) WithAppAdvisoryLock

func (s *PGStore) WithAppAdvisoryLock(ctx context.Context, appID string, fn func() error) error

WithAppAdvisoryLock runs fn while holding a transaction-scoped Postgres advisory lock for one app's deploy triggers (w8/m46), so two bex-api replicas cannot interleave a trigger's row image, CR patch and deploy-row open. The prefix keeps this lock domain apart from the workspace and subject locks above.

func (*PGStore) WithEnvGroupContentLock

func (s *PGStore) WithEnvGroupContentLock(ctx context.Context, id string, fn func(context.Context) error) (err error)

WithEnvGroupContentLock serializes a group's external value/projection effects across API replicas. A lease permits recovery only after the previous live writer releases its session (or crashes); it never licenses overwriting a stalled writer's later effects. The callback's group queries use this same connection, including detached compensation, so a one-connection pool works. Statements commit individually: a crash must retain the busy claim and op. The callback must not use the supplied context concurrently or retain it.

func (*PGStore) WithTenantAdvisoryLock

func (s *PGStore) WithTenantAdvisoryLock(ctx context.Context, tenantID string, fn func() error) error

WithTenantAdvisoryLock runs fn while holding a transaction-scoped Postgres advisory lock for tenantID. It coordinates count-then-write admission paths across bex-api replicas even when the write itself reaches an external provider before persisting its tenant binding.

func (*PGStore) WorkspaceForSandboxKey

func (s *PGStore) WorkspaceForSandboxKey(ctx context.Context, apiKey string) (string, error)

WorkspaceForSandboxKey resolves an OpenSandbox tenant key to its workspace id, returning ErrNotFound for an unknown key — which the tenant-lookup endpoint maps to the 401 the OpenSandbox HTTP tenant provider expects (invalid key).

type PaymentEligibility

type PaymentEligibility struct {
	Bound    bool
	Excluded bool
	Comped   bool
}

PaymentEligibility is the local paid-intent/export snapshot for one workspace. Excluded and comped are explicit operator exemptions; Bound is stamped only by the verified checkout.session.completed path (ADR046).

func (PaymentEligibility) AllowsPaidIntent

func (p PaymentEligibility) AllowsPaidIntent() bool

type PlanLimits

type PlanLimits struct {
	// MaxServices caps services per workspace, counting suspended ones (Render:
	// "Up to 25 services, all service types, including suspended"). 0 = unlimited.
	MaxServices int
	// MaxMembers caps members per workspace. Hobby is single-member (1); paid
	// plans are unlimited (0).
	MaxMembers int
	// MaxWorkspacesPerUser caps how many workspaces of this plan one user may
	// own. Render allows five free Hobby workspaces per user and unlimited paid
	// ones, so only Hobby sets this (5). 0 = unlimited.
	MaxWorkspacesPerUser int
	// AllowedRoles is the role set assignable on the plan — Render's plan-gated
	// role catalog (RESEARCH-workspaces.md finding 5, docs/render-artifacts/team-members.graphql):
	// Hobby is single-member (no invites, the sole member is always admin); Pro
	// adds Developer; Scale and Enterprise add Contributor, Viewer, and Billing.
	// Lowercase (the stored/FGA form), matching members.Roles.
	AllowedRoles []string
}

PlanLimits are the caps and capabilities a plan grants — the one place a plan's full shape is defined. A zero numeric field means "unlimited" — the paid plans lift Hobby's caps entirely (Render's Pro/Scale/Enterprise all allow unlimited members and services), so only Hobby carries non-zero caps.

func LimitsFor

func LimitsFor(plan string) PlanLimits

LimitsFor returns the caps and capabilities for a plan. Unknown plans get the (unlimited, full-role) paid shape rather than Hobby's — validation rejects unknown plans upstream, so this is only a safe default, never a silent downgrade.

type ProductInventoryCollector

type ProductInventoryCollector struct {
	Store    *PGStore
	Client   client.Client
	Identity string
}

ProductInventoryCollector runs off the reconciliation/request paths. A source is published atomically only after its entire authoritative list succeeds.

func (*ProductInventoryCollector) Collect

func (*ProductInventoryCollector) Run

type Project

type Project struct {
	ID        string    `json:"id"`
	TenantID  string    `json:"tenantId"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Project is a row of `projects` — a named grouping of services within a workspace. Services opt-in to a project by having project_id set in `apps`.

type PushNotification

type PushNotification struct {
	TenantID       string
	Subject        string
	SourceEventKey string
	EventID        string
	EventType      string
	Title          string
	Body           string
	Urgency        string
	ResourceKind   string
	ResourceID     string
	DeepLink       string
	OccurredAt     time.Time
	DeliverAt      time.Time
	CreatedAt      time.Time
	ReadAt         *time.Time
}

PushNotification is one durable logical inbox item. It is intentionally thin and token-free; clients fetch details through the authenticated API.

type PushNotificationBatchItem

type PushNotificationBatchItem struct {
	Notification PushNotification
	DeviceIDs    []string
}

PushNotificationBatchItem fans one logical notification out to these exact active device identities in the same transaction as the push watermark.

type PushQueueStats

type PushQueueStats struct {
	Pending        int64
	ReceiptPending int64
	Terminal       int64
}

type PushSweepResult

type PushSweepResult struct {
	RevokedDeliveries     int64
	TerminalNotifications int64
}

type QuotaCaps

type QuotaCaps struct {
	Services  int64
	Postgres  int64
	KeyValues int64
}

QuotaCaps are a plan's per-workspace object-count ceilings for Services/Postgres/KeyValues — the same numbers the per-namespace ResourceQuota (quotaForPlan, namespaces.go) enforces at the API server in place of the retired BEX_MAX_SERVICES/_POSTGRES/_KEYVALUES app-code caps (ADR043 D3). One source of truth so enforcement and the "3/5 services" display surface (workspaces.Service.ResourceLimits) can't drift apart.

func QuotaCapsForPlan

func QuotaCapsForPlan(plan string) QuotaCaps

QuotaCapsForPlan returns plan's object-count ceilings. Matches on PlanHobby (plus "" and "free" for legacy/test rows created before plan normalization) so every real Hobby workspace — whose stored plan is always "hobby" via NormalizePlan — gets the intended Render-Hobby-anchored ceiling rather than silently falling through to the generous paid default. Services reuses LimitsFor(PlanHobby).MaxServices — the same Render-Hobby-anchor number — rather than a second hardcoded literal, so the two catalogs can't drift.

type Reconciler

type Reconciler struct {
	// ProductObserver samples hosting/TLS without changing reconciliation intent.
	ProductObserver func(context.Context, DesiredApp, *appv1alpha1.App)

	Client client.Client
	Store  Store
	Resync time.Duration // full-resync interval
	// Identity names this control-plane instance (BEX_CP_IDENTITY). It is
	// stamped on every App CR this projector owns and scopes its delete-by-
	// absence pass. Empty is read as DefaultControlPlaneIdentity.
	Identity string
	// BuildNamespace is BEX_BUILD_NAMESPACE: where a release a suspend ended
	// has its build artifacts deleted (CancelRelease). Empty means the App's
	// own namespace.
	BuildNamespace string
	// DeployGateTimeout bounds how long a deploy may stay open before
	// recordDeploy closes it as failed even though the CR's phase never
	// reached Failed on its own (see defaultDeployGateTimeout).
	DeployGateTimeout    time.Duration
	BuildGateTimeout     time.Duration
	PreDeployGateTimeout time.Duration
	// CloneSecrets, when non-nil, is called for each new projected App CR
	// whose row has a non-empty Repo, to mint and write the per-app
	// clone-credential Secret. Useful for rows created via the internal CP
	// API (store/api.go) where the public-surface create path hasn't already
	// done so. Soft failure: a minting error is logged but the CR is still
	// created (public repos don't need a secret).
	CloneSecrets CloneSecreter

	// DeployNotifier, when non-nil, is called every time recordDeploy closes a
	// deploy as succeeded or failed — see DeployNotifier. nil => no emails
	// (notifications feature off / store off).
	DeployNotifier DeployNotifier

	// Metrics, when non-nil, counts the conclusions rejectStaleUnhealthy
	// refuses (w6/m41). nil => rejections are logged but not metered.
	Metrics *ReconcilerMetrics
	// contains filtered or unexported fields
}

Reconciler projects the source of truth into the cluster: each apps row (+ its domains) becomes an App CR; rows deleted from Postgres get their CR deleted; the CR's observed status (phase, url) is written back to the row. It is level-triggered — a full resync every Resync plus a Kick after API writes — so etcd stays a rebuildable projection of Postgres.

func NewReconciler

func NewReconciler(cl client.Client, store Store) *Reconciler

func (*Reconciler) Kick

func (r *Reconciler) Kick()

Kick schedules an immediate reconcile (non-blocking, coalescing). The API calls it after every successful write so a POST is projected within milliseconds instead of a resync period.

func (*Reconciler) ReconcileOnce

func (r *Reconciler) ReconcileOnce(ctx context.Context) error

ReconcileOnce drives one full pass: desired rows vs. existing managed CRs → create / update / delete, then copies each CR's status back to its row. Per-app failures are collected, not fatal — one bad row can't block the rest.

func (*Reconciler) Run

func (r *Reconciler) Run(ctx context.Context)

Run reconciles until ctx is done.

type ReconcilerMetrics

type ReconcilerMetrics struct {
	// contains filtered or unexported fields
}

ReconcilerMetrics makes the reconciler's refused conclusions a readable signal instead of an invisible suppression (w6/m41): a guard that silently drops observations is one incident away from being blamed for a missed outage. The healthy shape is zero in steady state, climbing during a control-plane incident (informer staleness is exactly the condition that produces time-traveled conclusions).

func NewReconcilerMetrics

func NewReconcilerMetrics(registerer prometheus.Registerer) *ReconcilerMetrics

func (*ReconcilerMetrics) Rejection

func (m *ReconcilerMetrics) Rejection(reason, subject string)

Rejection counts one refused conclusion. Both labels come from closed vocabularies — reason from rejectReason*, subject from rejectSubject* — never anything tenant-derived, so label cardinality stays bounded regardless of fleet size. subject separates the App observation path from the managed- datastore one (w3/m82): they share the guard but not the incident.

type RegistryCredential

type RegistryCredential struct {
	ID          string
	WorkspaceID string
	// Name is a human display label (Render's registryCredential.name) — the
	// caller may leave it empty at creation, in which case CreateRegistryCredential
	// defaults it to host.
	Name      string
	Host      string
	Username  string
	ExpiresAt *time.Time
	CreatedBy string
	CreatedAt time.Time
	UpdatedAt time.Time
}

RegistryCredential is a row of `registry_credentials` (w2/m14): the metadata for a workspace's stored credential to a private external image registry. The secret value is never a field here — it lives in OpenBao, resolved separately by the registrycreds feature (docs/ADR013-secrets.md, the same split the env-vars/secret-files feature uses).

type ResolveBillingExportIssueRequest

type ResolveBillingExportIssueRequest struct {
	Action string `json:"action"`
	Actor  string `json:"actor"`
	Reason string `json:"reason"`
}

type ResourceDisplayName

type ResourceDisplayName struct {
	Kind string // ResourceKind* — "service", "postgres", "key_value", "sandbox"
	ID   string
	Name string
}

ResourceDisplayName is one retained (kind, id) -> name record for a tenant. See migration 0121: the record outlives the resource so a charge line can still name what it is billing for.

type RetiredUsageResource

type RetiredUsageResource struct {
	ResourceKind string
	ResourceID   string
	WorkspaceID  string
	// Name is an App's service name (its pod identity derives from it) or a
	// datastore's CR name; Namespace, Display and Public are datastores' only.
	Name      string
	Namespace string
	Display   string
	Public    bool
	Tier      string
	CreatedAt time.Time
	RetiredAt time.Time
}

RetiredUsageResource is a deleted resource the usage meters still owe hours for, through the hour it was deleted in (w4/m173; ADR023). Apps are retired by a trigger on their row's deletion (migration 0151); datastores, which have no row, by their delete verbs through UsageRetirementRecorder.

func RetiredDatastoreFor

func RetiredDatastoreFor(obj client.Object) (r RetiredUsageResource, ok bool)

RetiredDatastoreFor is what metering needs from a Database or KeyValue CR about to be deleted; ok is false for anything else, or a CR no workspace owns (which the meters never listed).

type RoleReconciliation

type RoleReconciliation struct {
	TenantID  string
	Subject   string
	Role      string
	Attempts  int
	CreatedAt time.Time
	UpdatedAt time.Time
}

RoleReconciliation is one claimed Postgres -> OpenFGA exact-role repair. Subject is the raw identity id; callers add OpenFGA's user: prefix.

type SSHKey

type SSHKey struct {
	ID          string    `json:"id"`
	Subject     string    `json:"-"`
	Name        string    `json:"name"`
	PublicKey   string    `json:"publicKey"`
	Fingerprint string    `json:"fingerprint"`
	CreatedAt   time.Time `json:"createdAt"`
}

SSHKey is one identity-owned public key. Private key material never enters the API or store; Fingerprint is the canonical OpenSSH SHA256 fingerprint.

type SSHSessionAudit

type SSHSessionAudit struct {
	ID            string
	Subject       string
	WorkspaceID   string
	ServiceID     string
	InstanceID    string
	RemoteAddress string
	StartedAt     time.Time
}

SSHSessionAudit contains connection metadata only. Its schema deliberately has no command, environment, or stream-content column, making terminal-data capture impossible through this persistence seam.

type SandboxMeterObservation

type SandboxMeterObservation struct {
	WorkspaceID string
	SandboxID   string
	Phase       string
	Tier        string
	WeightMilli int64
	ObservedAt  time.Time
}

SandboxMeterObservation is one authoritative OpenSandbox phase sample. WeightMilli is the sandbox shape expressed as milli-vCPU equivalents, with memory folded in at the documented AgentCore CPU:memory price ratio.

type SandboxTenantKey

type SandboxTenantKey struct {
	WorkspaceID string
	APIKey      string
}

SandboxTenantKey is one workspace's OpenSandbox tenant credential. It is used only by the internal metering poller and must never be logged.

type SandboxTenantResolver

type SandboxTenantResolver interface {
	WorkspaceForSandboxKey(ctx context.Context, apiKey string) (string, error)
}

SandboxTenantResolver maps an OpenSandbox tenant key to its workspace id. Satisfied by *PGStore (WorkspaceForSandboxKey); injected by cmd/api so the store's big Store interface (and its fakes) need not carry the method.

type SandboxUsageMetadata

type SandboxUsageMetadata struct {
	Name  string
	Phase string
	Tier  string
}

SandboxUsageMetadata keeps historical session labels separate from the last observed lifecycle. An empty Phase means unknown; a label never proves liveness.

type ServiceEventFact

type ServiceEventFact struct {
	SourceKey  string
	AppID      string
	Type       ServiceEventFactType
	At         time.Time
	DeployID   string
	Image      string
	ReasonCode string
	InstanceID string
	FromCount  *int32
	ToCount    *int32
	BranchFrom string
	BranchTo   string
	CommitID   string
	CommitURL  string
	// Status is the terminal outcome of a lifecycle-step fact (build_ended,
	// pre_deploy_ended, job_run_ended): one of EventStatus* or "" for the
	// started/observed kinds that have no outcome. Closed set (w7/m66).
	Status string
	// ExitCode is a cron_job_run_ended detail (w4/m114): a failed run's
	// container exit status (with ReasonCode non_zero_exit or oom_killed).
	// RunID is the run's public crr- id on both cron_job_run_started (w4/259)
	// and cron_job_run_ended.
	ExitCode *int32
	RunID    string
}

ServiceEventFact is a closed, non-secret event record. SourceKey is a stable producer identity used for idempotency; it is never exposed directly.

func CanceledBuildLifecycleFacts

func CanceledBuildLifecycleFacts(open Deploy) []ServiceEventFact

CanceledBuildLifecycleFacts derives the build_started/build_ended pair (if any) for a deploy the Cancel verb closes directly (w6/m128) — the one lifecycle transition that bypasses the reconciler entirely, so recordDeploy never runs for it and buildLifecycleFacts is otherwise unreachable. It reuses that same rule rather than re-deriving it: canceled while queued still emits neither fact, canceled mid-build emits build_ended(canceled), and canceled after the build finished emits build_ended(succeeded). open must be the deploy row exactly as it stood before the cancel closed it.

type ServiceEventFactType

type ServiceEventFactType string

ServiceEventFactType is the closed vocabulary persisted outside deploy and audit rows. Keep this in the store package: the control-plane reconciler and signed Git webhook produce facts without importing the presentation feature.

const (
	EventFactImagePullFailed  ServiceEventFactType = "image_pull_failed"
	EventFactServiceSuspended ServiceEventFactType = "service_suspended"
	EventFactServiceResumed   ServiceEventFactType = "service_resumed"
	// Free-tier idle auto-sleep (w6/m47). Deliberately NOT the suspended/resumed
	// pair: those stay exclusively user-driven, so a webhook or push subscriber
	// watching for an unexpected suspension is not woken by every routine sleep
	// cycle of a free service.
	EventFactServiceHibernated  ServiceEventFactType = "service_hibernated"
	EventFactServiceWoken       ServiceEventFactType = "service_woken"
	EventFactServerFailed       ServiceEventFactType = "server_failed"
	EventFactServerAvailable    ServiceEventFactType = "server_available"
	EventFactBranchChanged      ServiceEventFactType = "branch_changed"
	EventFactBranchDeleted      ServiceEventFactType = "branch_deleted"
	EventFactCommitIgnored      ServiceEventFactType = "commit_ignored"
	EventFactAutoscalingStarted ServiceEventFactType = "autoscaling_started"
	EventFactAutoscalingEnded   ServiceEventFactType = "autoscaling_ended"
	// Deploy-lifecycle facts (w7/m66): the build, pre-deploy, and one-off-job
	// beats Render shows as distinct timeline entries. The *_ended kinds carry a
	// closed Status (succeeded|failed|canceled); the operator observes them
	// through the same control-plane reconciler path image_pull_failed rides.
	EventFactBuildStarted     ServiceEventFactType = "build_started"
	EventFactBuildEnded       ServiceEventFactType = "build_ended"
	EventFactPreDeployStarted ServiceEventFactType = "pre_deploy_started"
	EventFactPreDeployEnded   ServiceEventFactType = "pre_deploy_ended"
	EventFactJobRunEnded      ServiceEventFactType = "job_run_ended"
	EventFactCronRunStarted   ServiceEventFactType = "cron_job_run_started"
	EventFactCronRunEnded     ServiceEventFactType = "cron_job_run_ended"
)

type ServiceEventFilter

type ServiceEventFilter struct {
	// Since/Until bound At inclusively (Render's startTime/endTime).
	Since time.Time
	Until time.Time
	// AfterAt/AfterKey resume strictly after a previously returned event —
	// keyset paging on the feed's total order (At DESC, Key DESC), so a row
	// inserted between two pages shifts nothing already returned. Zero AfterAt
	// starts at the head.
	AfterAt  time.Time
	AfterKey string
	// Verbs are the audit verbs that map to an event type the caller asked for
	// (internal/events owns the mapping; the store never interprets a verb).
	Verbs []string
	// Phases are the deploy transitions the caller asked for (EventPhaseStarted
	// and/or EventPhaseEnded).
	Phases []string
	// FactTypes are the closed service_event_facts kinds requested by the caller.
	FactTypes []string
	// AutoDeploy pushes down the auto-deploy boolean discrimination into SQL when
	// the Verbs set includes apps.SetAutoDeploy: the OR of the sub-types to keep,
	// applied to SetAutoDeploy rows only. AutoDeployFilterNone (zero value) means
	// no additional constraint on auto_deploy_enabled.
	AutoDeploy AutoDeployFilter
	// Limit caps the page (<1 or >core.MaxPageLimit clamps to core.DefaultPageLimit).
	Limit int
}

ServiceEventFilter narrows ListServiceEvents.

Verbs and Phases are how the caller's event-TYPE filter is pushed down into SQL. They are the only two knobs the vocabulary needs, and passing them (rather than filtering the result in Go) is what keeps a page exactly `Limit` long: a Go-side filter after the LIMIT returns short — sometimes empty — pages, which a cursor client reads as the end of the feed and stops on. Empty Verbs excludes every audit row; empty Phases excludes every deploy row.

type ServiceEventLookup

type ServiceEventLookup struct {
	Event     ServiceEventRow
	ServiceID string
}

ServiceEventLookup is one globally-addressed event plus the resource identity materialized beside its source key. ServiceID is the canonical public id: apps.id (srv-…) for a service, or the typed dpg-/red- target for a datastore.

type ServiceEventRow

type ServiceEventRow struct {
	// Key is the row's stable identity within the feed: "<source row id>:<phase>"
	// for a deploy ("dep-abc:started"), "<audit row id>:" for an audit event. It
	// is the sort tiebreak and the cursor's second component — unique by
	// construction, since a source row id is unique and a row projects each phase
	// at most once. internal/events hashes it into the public evt-… id.
	Key string
	At  time.Time
	// Source is EventSourceDeploy, EventSourceAudit, or EventSourceFact.
	Source string
	// Phase is EventPhaseStarted/EventPhaseEnded for a deploy row; empty for audit.
	Phase string

	// Deploy rows only.
	DeployID string
	Trigger  string // "create" (the app's first deploy) | "api"; the started phase only
	Status   string // the deploy's terminal status; the ended phase only
	// PreDeployStatus is the deploy's pre-deploy step outcome (w1/m33): ” |
	// 'running' | 'succeeded' | 'failed'; the ended phase only.
	PreDeployStatus string
	// FailureReason is the human-actionable cause stamped when the reconciler
	// closes the deploy failed (w1/m138); the ended phase only. Empty on
	// non-failed deploys — the deploys table stores it that way.
	FailureReason string
	// CancelReason is the neutral cause of a non-user cancel (w4/089) —
	// "Superseded by dep-…", or the suspend that ended the rollout (w4/m171) —
	// on deploy_ended and on lifecycle facts joined to that deploy. Empty for
	// user cancels and non-canceled rows.
	CancelReason string
	// StallReason is why an OPEN deploy is not progressing (w4/m112) — the
	// operator's live diagnosis of the current revision's pods. The STARTED
	// phase only, and only while the deploy is open: the row's own column is
	// cleared as it goes terminal. An observation, not a verdict.
	StallReason string
	// Deployed image URI; empty for non-deploy rows. (w1/m47)
	Image string
	// Commit ID (git revision); empty for non-deploy rows. (w1/m47)
	CommitID string
	// Commit message; empty for non-deploy rows. (w1/m47)
	CommitMessage string
	// When the deploy started executing; nil for non-deploy or not-yet-started. (w1/m47)
	StartedAt *time.Time
	// When the deploy finished (terminal status reached); nil for non-deploy or ongoing. (w1/m47)
	FinishedAt *time.Time

	// Audit rows and deploy rows (w4/072). Same subject form as audit_events.caller.
	Verb   string // e.g. "apps.Suspend"; empty on deploy rows
	Caller string // core.Identity.Subject; empty when the source has none
	// Typed per-verb detail fields from audit_events — nil for every other verb.
	PlanFrom           *string
	PlanTo             *string
	InstanceCountFrom  *int32
	InstanceCountTo    *int32
	AutoscalingMinFrom *int32
	AutoscalingMaxFrom *int32
	AutoscalingMinTo   *int32
	AutoscalingMaxTo   *int32
	AutoDeployEnabled  *bool
	// service_moved placement pair (w6/m134): public prj-/env- ids, nil = no
	// placement on that side. Nil for every other verb.
	ProjectFrom     *string
	ProjectTo       *string
	EnvironmentFrom *string
	EnvironmentTo   *string
	// Managed-datastore configuration values (w3/m82): what a field-level
	// datastore effect set its field TO. Nil for every other verb.
	HighAvailabilityEnabled *bool
	ConnectionPoolEnabled   *bool
	DiskSizeGB              *int32
	MaxmemoryPolicy         *string
	PersistenceMode         *string

	// Typed service_event_facts columns. FactType is the closed discriminator;
	// all remaining values are bounded scalars used only by the types that own
	// them. Image and CommitID reuse the deploy columns above.
	FactType   string
	ReasonCode string
	InstanceID string
	FromCount  *int32
	ToCount    *int32
	BranchFrom string
	BranchTo   string
	CommitURL  string
	// FactStatus is a lifecycle-step fact's terminal outcome (build_ended,
	// pre_deploy_ended, job_run_ended): succeeded|failed|canceled, or "" for the
	// started/observed kinds. A distinct column from the deploy-arm Status above,
	// which carries a deploy row's terminal status (w7/m66).
	FactStatus string
	// FactExitCode belongs to cron_job_run_ended (w4/m114): the failed run's
	// container exit status. FactRunID is the run's public crr- id on cron
	// starts (w4/259) and ends.
	FactExitCode *int32
	FactRunID    string
}

ServiceEventRow is one row of the composed feed — the raw projection, before internal/events maps it onto Render's event vocabulary. Deploy rows fill DeployID/Trigger/Status and Caller (the opener's subject, w4/072); audit rows fill Verb/Caller and the typed per-verb detail fields. No column here can carry a free-form value: deploy rows hold ids, a status enum, and the same identity subject audit_events.caller stores; audit rows hold a verb name, a caller subject, and typed scalars mirroring audit_events' typed columns.

type SetBillingExcludedRequest

type SetBillingExcludedRequest struct {
	Excluded bool   `json:"excluded"`
	Actor    string `json:"actor,omitempty"`
}

SetBillingExcludedRequest is the PATCH /v1/tenants/{id}/billing-excluded body. Actor attributes the audit row (defaults to "control-plane").

type Store

type Store interface {
	CreateTenant(ctx context.Context, name, plan string) (Tenant, error)
	ListTenants(ctx context.Context) ([]Tenant, error)
	// GetTenant and CountAppsForTenant back the per-plan service cap the create
	// path enforces (w6/m1): a Hobby workspace is capped at 25 apps.
	GetTenant(ctx context.Context, id string) (Tenant, error)
	CountAppsForTenant(ctx context.Context, tenantID string) (int, error)
	// SetTenantBillingExcluded flips a workspace's Stripe billing-exclusion
	// flag (docs/ADR040-billing-metronome.md §7), auditing the change. Admin-only
	// — the control-plane internal API is its sole caller. Returns whether the
	// value changed; ErrNotFound for an unknown workspace.
	SetTenantBillingExcluded(ctx context.Context, tenantID string, excluded bool, actor string, at time.Time) (bool, error)
	// TenantForIdentity returns the subject's DEFAULT workspace (w6/m14): the
	// tenant of its OLDEST membership in tenant_members, or ErrNotFound. One
	// lookup serves both human (Kratos identity id) and machine (Hydra client
	// id) callers since tenant_members.subject covers both kinds of id.
	//
	// The default-workspace contract: a subject may belong to several
	// workspaces (w6/m1 multi-workspace, w4/m12 invites), so this is the
	// implicit resolution used when a caller names no workspace — deterministic
	// by membership created_at (tenant id as the tie-break), never an arbitrary
	// row of the join. Oldest-first makes it the workspace the caller has had
	// longest, which for a human is the personal tenant minted on first login,
	// matching Render ("passing a user id returns that user's default
	// workspace"). A caller that wants another of its workspaces names it
	// explicitly (REST ownerId / GraphQL ownerId / MCP workspaceId →
	// core.WithWorkspace), which is membership-checked at core.Base.
	TenantForIdentity(ctx context.Context, subject string) (Tenant, error)
	// IsMember reports whether a subject belongs to a workspace — the check
	// core.Base runs before honoring an explicit workspace override, so naming
	// a workspace the caller is not a member of is refused (ErrForbidden)
	// rather than silently redirected to the caller's own (w6/m14).
	IsMember(ctx context.Context, subject, tenantID string) (bool, error)
	// CreateTenantWithMember mints a personal tenant for an identity on first
	// login: a tenant row owned by the identity plus an admin membership. It is
	// idempotent and race-safe — concurrent first logins for the same identity
	// yield exactly one tenant (the partial unique index on owner_identity_id
	// is the gate, not a check-then-insert). The tenant name is a placeholder
	// (its id) pending a future rename API.
	CreateTenantWithMember(ctx context.Context, identityID, plan string) (Tenant, error)
	// AddMember records a subject's membership in a tenant (idempotent). The
	// platform tenant-create path uses it to make the Admin identity a member —
	// without the row the resolver can't map that identity to its workspace.
	AddMember(ctx context.Context, subject, tenantID, role string) error
	// BindClient records that an API key belongs to a tenant — a tenant_members
	// row keyed by the key's client_id, the same table TenantForIdentity reads
	// (idempotent — a re-bind to the same or another tenant upserts). The
	// api-keys mint calls this after creating the Hydra client.
	BindClient(ctx context.Context, clientID, tenantID string) error
	// UnbindClient removes an API key's tenant binding (idempotent — a key that
	// was never bound is not an error). The api-keys revoke calls this.
	UnbindClient(ctx context.Context, clientID string) error
	CreateApp(ctx context.Context, a App) (App, error)
	GetApp(ctx context.Context, id string) (App, error)
	ListApps(ctx context.Context) ([]App, error)
	DeleteApp(ctx context.Context, id string) error
	CreateDomain(ctx context.Context, appID, host string, primary bool) (Domain, error)
	ReplaceDomains(ctx context.Context, appID, primary string, hosts []string) error
	// DeleteDomain removes a custom domain row. Not-found is ErrNotFound.
	DeleteDomain(ctx context.Context, appID, host string) error
	// Persistent service disks (docs/ADR082-persistent-disks.md). The row is
	// the source of truth for both the projected spec.disk and the provisioned
	// GB-seconds meter, which is why every write also closes or opens a size
	// period — see service_disk_sizes in migration 0096.
	//
	// CreateDisk returns ErrConflict when the service already has a live disk:
	// a service can carry at most one, and the unique index enforces it even
	// against a racing second create.
	CreateDisk(ctx context.Context, tenantID, appID, name, mountPath string, sizeGB int32) (Disk, error)
	GetDisk(ctx context.Context, id string) (Disk, error)
	// ListDisks returns the workspace's live disks, newest first. An empty
	// appID lists them all; a non-empty one narrows to a single service.
	ListDisks(ctx context.Context, tenantID, appID string) ([]Disk, error)
	// UpdateDisk applies a grow and/or a rename/remount. sizeGB below the
	// current size is ErrInvalid — a disk never shrinks (ADR082 D2).
	UpdateDisk(ctx context.Context, id string, name, mountPath *string, sizeGB *int32) (Disk, error)
	// DeleteDisk soft-deletes the row, which both detaches the volume (the
	// projector clears spec.disk) and stops the meter by closing the open size
	// period. The row is retained so an already-billed period stays auditable.
	DeleteDisk(ctx context.Context, id string) error
	// DiskUsageForWindow integrates provisioned disk GB-seconds per service
	// over [from, to); LatestUsageWindowForKind is that meter's cursor.
	DiskUsageForWindow(ctx context.Context, from, to time.Time) ([]DiskUsageRow, error)
	LatestUsageWindowForKind(ctx context.Context, kind string) (time.Time, bool, error)
	ListDesiredApps(ctx context.Context) ([]DesiredApp, error)
	// SetAppSuspended flips the row's suspended flag — the single write path
	// for suspend/resume on store-managed Apps. bex-api's lifecycle verbs call
	// this (row first, then the CR fast-path) so the projection loop never
	// reverts a suspend it didn't know about.
	SetAppSuspended(ctx context.Context, id string, suspended bool) error
	// SetAppTier updates the row's tier — the single write path for plan
	// changes on store-managed Apps, same row-first rationale as
	// SetAppSuspended.
	SetAppTier(ctx context.Context, id string, tier string) error
	// SetAppReplicas updates the row's replica count — the single write path
	// for the manual-scale verb on store-managed Apps, same row-first
	// rationale as SetAppSuspended (the projector owns spec.replicas).
	SetAppReplicas(ctx context.Context, id string, replicas int32) error
	// SetAppIdleTTL updates the row's idle-TTL — the single write path for the
	// idle-timeout verb on store-managed Apps (the projector owns
	// spec.idleTTLSeconds), same row-first rationale as SetAppReplicas.
	SetAppIdleTTL(ctx context.Context, id string, seconds int32) error
	// SetAppDisplayName mirrors the App CR's mutable human label onto the row.
	// Unlike its neighbours here the row is NOT the writer of truth: the CR's
	// spec.displayName is, and this is only its read projection, kept so a
	// store-side query can resolve the label without a per-row k8s read
	// (w6/m101 — the workspace-wide event feed, webhook and push dispatch
	// alike, is its only reader).
	SetAppDisplayName(ctx context.Context, id string, displayName string) error
	// SetAppSource atomically updates the projector-owned source tuple and its
	// context-sensitive registry credential binding.
	SetAppSource(ctx context.Context, id, repo, image, branch string, registryCredentialID *string) error
	// BackfillAppType records the service type of a row created before the type
	// column existed (w6/m46 t001), read off the row's own live App CR. It is a
	// one-way heal, never a rename: the type is immutable, so it only ever
	// writes a row whose type is still empty and reports whether it wrote. Not a
	// lifecycle verb — the projector is its sole caller.
	BackfillAppType(ctx context.Context, id, serviceType string) (bool, error)
	// SetAppImage updates the row's image — the single write path for a
	// rollback's restored image on store-managed Apps (the projector owns
	// spec.image), same row-first rationale as SetAppReplicas (w2/m10).
	SetAppImage(ctx context.Context, id string, image string) error
	// GetEnvironmentProtectedStatus resolves an Environment's destructive-verb
	// protection state for Database/KeyValue members whose membership is stored
	// as a CR label rather than in a control-plane resource row.
	GetEnvironmentProtectedStatus(ctx context.Context, environmentID string) (string, error)

	// UpsertUsageHourly writes one window row idempotently (ON CONFLICT DO
	// UPDATE) — the write path for the metering loop (w8/m1). Re-processing
	// the same window is safe.
	UpsertUsageHourly(ctx context.Context, row HourlyRow) error
	// RecordUsageSourceHealth persists explicit source evidence for a window
	// that could not produce a usage row. Missing evidence remains unknown.
	RecordUsageSourceHealth(ctx context.Context, records []UsageSourceRecord) error
	// ReconcileUsageSourceStreams closes streams for resources no longer in the
	// collector inventory, without deleting their current-month evidence.
	ReconcileUsageSourceStreams(ctx context.Context, active []UsageResourceRef, through time.Time) error
	// CurrentUsageCoverage aggregates explicit current-month source evidence.
	CurrentUsageCoverage(ctx context.Context, workspaceID string, now time.Time) (UsageCoverage, error)
	// LatestUsageWindow returns the most-recent window_start for one resource
	// and meter kind so each meter can catch up independently after a restart.
	// Returns zero time when no matching rows exist yet.
	LatestUsageWindow(ctx context.Context, resourceKind, serviceID, kind string) (time.Time, error)
	// UsageMonthToDate returns month-to-date aggregates (grouped by service /
	// kind / tier) for a workspace, bounded by the caller-supplied now so tests
	// don't depend on wall time. Sums usage_hourly and usage_monthly together,
	// so the result is exact whether or not the month has been compacted.
	UsageMonthToDate(ctx context.Context, workspaceID string, now time.Time) ([]UsageSummaryRow, error)
	// CompactUsage folds hourly rows older than before into usage_monthly and
	// purges them — atomic and idempotent; the retention loop (w8/m4) calls it
	// daily with the hot-window boundary.
	CompactUsage(ctx context.Context, before time.Time) (UsageCompaction, error)

	// CreateDeploy opens a new deploy row for appID (status DeployCreated) —
	// CreateApp calls this for an app's first deploy
	// (trigger "create"); the deploys feature's Trigger verb calls it for an
	// explicit redeploy (trigger "api"). generation is the App CR's
	// metadata.generation this deploy runs under, captured once at open time
	// (w2/m10) — Cancel's build-Job identity is derived from this stored
	// value, not a fresh re-fetch, so a later unrelated spec write can't make
	// it compute the wrong Job name. commit is the resolved commit this deploy
	// runs (w9/001), zero when unresolvable. triggeredBy is
	// core.Identity.Subject for the caller that opened the row (w4/072), or
	// "" for git auto-deploy / deploy-hook / system paths. The reconciler's
	// write-back closes it.
	CreateDeploy(ctx context.Context, appID, trigger, image string, generation int64, commit CommitInfo, triggeredBy string) (Deploy, error)
	// LatestDeployCommit returns the newest non-empty commit for the app, or
	// zero CommitInfo when none exists (image-backed history, or only empty
	// rows). Used by rollout.Tracker to carry a prior release's provenance
	// onto config_change re-rolls — CreateDeploy itself never inherits.
	LatestDeployCommit(ctx context.Context, appID string) (CommitInfo, error)
	// CreateRollbackDeploy opens a new deploy row for appID whose trigger is
	// "rollback" and whose image/resolvedImage are the target being restored
	// — Render models rollback as a fresh deploy, never a history rewrite
	// (w2/m10). rollbackOf records provenance: the source deploy id being
	// rolled back to; commit is the target's own commit metadata (w9/001),
	// copied rather than re-resolved against a branch that has since moved.
	// triggeredBy is core.Identity.Subject for the rollback caller (w4/072).
	CreateRollbackDeploy(ctx context.Context, appID, image, rollbackOf string, generation int64, commit CommitInfo, triggeredBy string) (Deploy, error)
	// ListDeploys returns an app's deploy history, newest first, narrowed by
	// filter (w2/m31) — a zero DeployFilter returns the full history, the
	// pre-m31 contract.
	ListDeploys(ctx context.Context, appID string, filter DeployFilter) ([]Deploy, error)
	// GetDeploy fetches one deploy scoped to appID — a deployID belonging to a
	// different app is ErrNotFound, not a cross-app leak.
	GetDeploy(ctx context.Context, appID, deployID string) (Deploy, error)
	// ListOpenDeploys returns every non-terminal deploy across all apps in one
	// query — the reconciler's write-back hook
	// calls this once per ReconcileOnce pass and looks apps up in the result,
	// rather than one query per app in its per-app loop.
	ListOpenDeploys(ctx context.Context) ([]Deploy, error)
	// TransitionDeploy applies one legal lifecycle transition, advances
	// updated_at only when the status actually changes, stamps started_at on
	// the first dispatch-observing phase (see DeployStatusStampsDispatch) and
	// finished_at on terminal transitions, and atomically deactivates a prior
	// live deploy when this one reaches live. failureReason (w9/011) is stored
	// with the same transition when non-empty — pass it only alongside a
	// failure status. startedAt is observed execution-start evidence (the
	// operator's recorded build window, w6/m123), applied only when the row
	// carries none — a terminal skip (failure, or live first observed at live,
	// w4/m156) otherwise leaves started_at honestly null rather than
	// fabricating one; pass nil without evidence.
	// A stale/repeated/invalid transition returns false without changing data.
	// cancelReason (w4/089) is stored with the same transition when non-empty —
	// pass it only alongside DeployCanceled from the reconciler's supersede or
	// suspend close; deploys.Cancel / CloseDeploy leave it empty.
	TransitionDeploy(ctx context.Context, id, status, resolvedImage, failureReason, failureCode, cancelReason string, startedAt *time.Time) (bool, error)
	// DeployIDByGeneration returns the deploy id for appID at generation, or
	// "" when none exists (w4/089 supersede naming). Newest wins if somehow
	// multiple rows share a generation.
	DeployIDByGeneration(ctx context.Context, appID string, generation int64) (string, error)
	// CloseDeploy is the terminal-transition compatibility seam used by the
	// deploy service's Cancel path. It delegates to TransitionDeploy.
	CloseDeploy(ctx context.Context, id, status, resolvedImage string) (bool, error)
	// SetDeployPreDeployStatus records the pre-deploy step's outcome (” |
	// 'running' | 'succeeded' | 'failed') on a deploy row (w1/m33), projected from
	// the App CR's status.preDeploy by the reconciler. No-op when unchanged;
	// returns whether a row was updated.
	SetDeployPreDeployStatus(ctx context.Context, id, status string) (bool, error)
	// SettleCanceledPreDeploy gives a canceled release's deploy the verdict
	// ('succeeded' | 'failed') of the pre-deploy step its close settled as
	// 'canceled' while it still ran, and records the step's pre_deploy_ended
	// fact at finishedAt (w5/085). Returns whether a row was settled; a repeat,
	// or any other row, is a no-op.
	SettleCanceledPreDeploy(ctx context.Context, appID string, generation int64, status string, finishedAt time.Time) (bool, error)
	// SetDeployStallReason records why an OPEN deploy is not progressing, or
	// clears it with "" (w4/m112). Projected from the App CR's Ready condition
	// each pass; ignored once the row is terminal. Best-effort observation —
	// never a verdict, and never a reason to fail reconciliation.
	SetDeployStallReason(ctx context.Context, id, reason string) error
	// RecordObservedServiceState persists level-triggered App status edges through
	// a typed checkpoint; repeated reconciler observations are no-ops.
	RecordObservedServiceState(ctx context.Context, obs ObservedServiceState) ([]ServiceEventFact, error)
	// LastHealthyTransitionAt returns the Ready=True transition time recorded
	// with the service's current healthy checkpoint — the reference the
	// reconciler's stale-conclusion guard (w6/m41) orders an unhealthy edge
	// against. Zero when there is no healthy checkpoint or its transition time
	// is unknown; callers must fail open toward recording, never silence.
	LastHealthyTransitionAt(ctx context.Context, appID string) (time.Time, error)
	// RecordObservedDatastoreState is RecordObservedServiceState for a managed
	// Database or KeyValue (w3/m82): the same checkpoint CAS keyed on the
	// datastore's own dpg-/red- id, emitting the availability edges Render
	// names postgres_unavailable/postgres_available and
	// key_value_unhealthy/key_value_available.
	RecordObservedDatastoreState(ctx context.Context, obs ObservedDatastoreState) ([]DatastoreEventFact, error)
	// LastDatastoreHealthyTransitionAt is LastHealthyTransitionAt's datastore
	// twin, with the same fail-open-toward-recording contract.
	LastDatastoreHealthyTransitionAt(ctx context.Context, datastoreID string) (time.Time, error)
	InsertServiceEventFact(ctx context.Context, fact ServiceEventFact) (bool, error)
	InsertServiceEventFacts(ctx context.Context, facts []ServiceEventFact) error
}

Store is the persistence boundary. The API writes through it, the reconciler reads desired state and projects it into App CRs. Observed state (phase/url) is not persisted — it stays on the CR. The production implementation is PGStore; tests use an in-memory fake.

type StripeBillingEvent

type StripeBillingEvent struct {
	EventID           string
	EventType         string
	WorkspaceID       string
	CustomerID        string
	SubscriptionID    string
	ObjectID          string
	Livemode          bool
	ProviderCreatedAt time.Time
	ReceivedAt        time.Time
	Outcome           string
	Reason            string
}

StripeBillingEvent is the normalized, non-sensitive subset retained from a signature-verified event. Outcome is failure or success; the full body, payment method, card, and customer billing details are never persisted.

type Tenant

type Tenant struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Plan      string    `json:"plan"`
	CreatedAt time.Time `json:"createdAt"`
}

Tenant is a row of `tenants` — who owns apps; plan names the tier ladder row.

type TenantMember

type TenantMember struct {
	TenantID  string    `json:"tenantId"`
	Subject   string    `json:"subject"`
	Role      string    `json:"role"`
	CreatedAt time.Time `json:"createdAt"`
	// Kind is MemberKindUser for a person and MemberKindMachine for an API
	// key's binding (w5/m103). Both rows authorize; only people are members.
	Kind string `json:"kind"`
}

TenantMember is a row of `tenant_members` — a subject's membership of a workspace and their role. Subject is the OpenFGA user (Kratos identity id or Hydra client id) granted the matching relation on workspace:<tenant-id>.

type TerminalTurnFact

type TerminalTurnFact struct {
	Turn       int
	AcceptedAt time.Time
	StartedAt  *time.Time // nil when the turn never reached running (no bind)
	TerminalAt time.Time
}

TerminalTurnFact is the immutable timing for a successful terminal CAS on the session's current turn (w5/m88). Callers observe metrics only when Finalize* succeeds; ErrNotFound means another writer already owned the transition.

type UsageCompaction

type UsageCompaction struct {
	Months     int64 // distinct calendar months compacted
	HourlyRows int64 // usage_hourly rows purged
}

UsageCompaction reports what one CompactUsage pass did — the operational summary the compaction loop logs.

type UsageCoverage

type UsageCoverage struct {
	Known           bool
	Complete        bool
	Through         time.Time
	DegradedSources []string
}

UsageCoverage is the store's current-month evidence aggregate.

type UsageExportAttempt

type UsageExportAttempt struct {
	Row           HourlyRow
	TransactionID string
	EventName     string
}

UsageExportAttempt binds the durable outbox row to the deterministic Stripe identifier and meter name selected by the billing mapper.

type UsageExportReject

type UsageExportReject struct {
	Attempt UsageExportAttempt
	Code    string
	Message string
}

UsageExportReject is the non-secret, bounded context retained for a permanent provider rejection.

type UsageResourceRef

type UsageResourceRef struct {
	ResourceKind string `json:"resourceKind"`
	ServiceID    string `json:"serviceId"`
}

UsageResourceRef is the current collector inventory. Reconciliation closes streams for deleted resources so they do not hold the common watermark back forever; their historical health rows remain intact.

type UsageRetirementRecorder

type UsageRetirementRecorder interface {
	RecordRetiredUsageResource(ctx context.Context, r RetiredUsageResource) error
}

UsageRetirementRecorder records a datastore's retirement before its CR is deleted.

type UsageSourceObservation

type UsageSourceObservation struct {
	Source       string
	State        string
	ExpectedFrom time.Time
}

UsageSourceObservation is one source's accounting health for an hourly usage row. ExpectedFrom is the earliest hour the resource was expected to contribute; it prevents a mid-month migration from blessing legacy gaps.

type UsageSourceRecord

type UsageSourceRecord struct {
	WorkspaceID  string
	ResourceKind string
	ServiceID    string
	Kind         string
	WindowStart  time.Time
	UsageSourceObservation
}

UsageSourceRecord persists health when no usage row can be written (for example a transport error). It makes partial evidence explicit instead of relying on a missing row, which remains unknown/legacy.

type UsageSummaryRow

type UsageSummaryRow struct {
	ServiceID    string
	Kind         string
	Tier         string // non-empty only for instance_seconds
	ResourceKind string // ResourceKindService / ResourceKindPostgres / ResourceKindKeyValue
	Total        int64
}

UsageSummaryRow is one resource/meter-kind/tier aggregate as returned by UsageMonthToDate — the raw numbers m2's core verb formats for adapters.

type WebPushSubscription

type WebPushSubscription struct {
	TenantID         string     `json:"tenantId"`
	Subject          string     `json:"subject"`
	BrowserID        string     `json:"browserId"`
	Endpoint         string     `json:"-"`
	P256dh           string     `json:"-"`
	Auth             string     `json:"-"`
	EndpointDigest   string     `json:"-"`
	PreferenceID     string     `json:"preferenceRef,omitempty"`
	RevokedAt        *time.Time `json:"-"`
	CreatedAt        time.Time  `json:"createdAt"`
	UpdatedAt        time.Time  `json:"updatedAt"`
	LastRegisteredAt time.Time  `json:"lastRegisteredAt"`
}

WebPushSubscription is one browser PushSubscription. Endpoint, keys, and digest are internal delivery capabilities and are omitted from JSON.

type WebhookAttempt

type WebhookAttempt struct {
	ID             string
	NotificationID string
	EndpointID     string
	EventID        string
	EventType      string
	ServiceID      string
	AttemptNumber  int
	Status         string
	StatusCode     int
	TransportError string
	ResponseBody   string
	Payload        string
	SentAt         *time.Time
	Origin         string
	RequestedBy    string
	IdempotencyKey string
	ParentStatus   string
	NextAttemptAt  *time.Time
	// ResumeAutomaticAt is internal queue state: a manual reservation parks an
	// unsent automatic retry here and restores it only if the manual send fails.
	ResumeAutomaticAt *time.Time
	CreatedAt         time.Time
}

WebhookAttempt is one scheduled send and its immutable terminal evidence. NotificationID identifies the logical endpoint/event parent. Pending rows reserve an ID before a send; the worker fills SentAt and outcome exactly once. Payload is joined from the parent so evidence never copies request bytes.

type WebhookAttemptCompletion

type WebhookAttemptCompletion struct {
	AttemptID      string
	NextAttemptID  string
	StatusCode     int
	TransportError string
	ResponseBody   string
	CompletedAt    time.Time
	NextAttemptAt  time.Time
	Delivered      bool
	Exhausted      bool
	DisableReason  string
}

WebhookAttemptCompletion names terminal evidence and parent-transition controls that would otherwise be ambiguous adjacent values and booleans.

type WebhookAttemptFilter

type WebhookAttemptFilter struct {
	EndpointID string
	SentAfter  time.Time
	SentBefore time.Time
	Status     string
	AfterAt    time.Time
	AfterKey   string
	Limit      int
}

WebhookAttemptFilter is one bounded immutable-attempt history query.

type WebhookDelivery

type WebhookDelivery struct {
	ID              string
	EndpointID      string
	EventID         string
	EventType       string
	ServiceID       string
	Payload         string
	AttemptCount    int
	LastStatus      int
	LastError       string
	ResponseBody    string
	NextAttemptAt   time.Time
	SentAt          *time.Time
	LastAttemptedAt *time.Time
	DeliveredAt     *time.Time
	FailedAt        *time.Time
	CreatedAt       time.Time
}

WebhookDelivery is the logical parent row for one event × one subscribed endpoint. Immutable send evidence lives in WebhookAttempt child rows.

type WebhookEligibility

type WebhookEligibility struct {
	EnabledAt  time.Time
	DisabledAt *time.Time
}

WebhookEligibility is one stretch an endpoint was enabled (w4/m212): [EnabledAt, DisabledAt), DisabledAt nil while it still is. A source event is eligible for the endpoint only when its time falls inside one.

func (WebhookEligibility) Contains

func (e WebhookEligibility) Contains(at time.Time) bool

Contains reports whether a source event at `at` falls inside the interval.

type WebhookEndpoint

type WebhookEndpoint struct {
	ID             string
	TenantID       string
	Name           string
	URL            string
	Secret         string
	EventTypes     []string
	Enabled        bool
	DisabledReason string
	CreatedBy      string
	CreatedAt      time.Time
	UpdatedAt      time.Time
	// Eligibility is populated only by ListWebhookDispatchEndpoints.
	Eligibility []WebhookEligibility
	// Latest* is the newest completed immutable attempt for this endpoint and
	// the state of that attempt's logical notification, fetched in the same
	// query as the endpoint rather than by one history query per endpoint.
	// Populated by every read and every mutation response since w4/117 — it was
	// list-only, so a single-endpoint read reported an endpoint with four
	// successful deliveries as having never delivered anything.
	LatestAttemptStatus string
	LatestAttemptAt     *time.Time
	LatestParentStatus  string
}

WebhookEndpoint is a row of `webhook_endpoints`. Secret is populated only by CreateWebhookEndpoint (the mint-once read) and by the sender's DueWebhookDeliveries join (it signs with it) — every caller-facing read (List/Get) leaves it "", structurally: those queries never select the column.

type WebhookEnqueueResult

type WebhookEnqueueResult struct {
	Admitted     int
	Capped       int
	Deduplicated int
}

WebhookEnqueueResult is aggregate, bounded evidence from one dispatcher commit. It deliberately carries no workspace, endpoint, event, URL, or payload dimension: callers may log/meter it without turning tenant-controlled values into an observability cardinality or confidentiality problem.

type WebhookEventRow

type WebhookEventRow struct {
	// CursorAt is when the source row became dispatch-visible. It normally equals
	// At; late-persisted observed facts use service_event_facts.recorded_at so an
	// older occurrence timestamp cannot fall behind the durable watermark.
	CursorAt    time.Time
	Key         string
	At          time.Time
	TenantID    string
	ServiceID   string
	ServiceName string
	Source      string // EventSourceDeploy | EventSourceAudit | EventSourceFact
	Phase       string // deploy rows: EventPhaseStarted | EventPhaseEnded
	DeployID    string
	// Status is the deploy's terminal status on the ended phase, and the fact's
	// own status on fact rows. Empty elsewhere.
	Status string
	Verb   string // audit rows: e.g. "apps.Suspend"
	// AutoDeployEnabled discriminates apps.SetAutoDeploy into Render's enabled
	// and disabled event types. nil on every other row (and on legacy audit rows).
	AutoDeployEnabled *bool
	FactType          string // fact rows: closed service_event_facts.fact_type
	// AppID is the app's internal control-plane id, as opposed to ServiceID's
	// public composite. Empty on the datastore audit arm, which has no app.
	AppID string
}

WebhookEventRow is one workspace-attributed row of the dispatcher's composed feed — the same projection ServiceEventRow carries, plus the tenant and service identity the join contributes (which the per-service feed takes as parameters instead). ServiceID is the PUBLIC service id — the projected CR name "<tenantName>-<appName>" (core.CRName), what GET /v1/services/{id} accepts — so a webhook receiver can turn a payload straight into an API call; ServiceName is the human label that service is shown under everywhere else — its displayName once renamed, else the immutable name (appDisplayLabel), so a payload never reports a stale creation-time name. Datastore rows carry the audit row's own target_name.

type WebhookResendRequest

type WebhookResendRequest struct {
	TenantID        string
	EndpointID      string
	SourceAttemptID string
	RequestedBy     string
	IdempotencyKey  string
	RequestedAt     time.Time
}

WebhookResendRequest names the several identities at the store seam so owner, endpoint, source-attempt, and caller IDs cannot be accidentally transposed.

type WorkspaceCreationAttempt

type WorkspaceCreationAttempt struct {
	ID                      string
	WorkspaceID             string
	OwnerSubject            string
	Name                    string
	Plan                    string
	BillingEmail            string
	PaymentRequired         bool
	State                   string
	ProviderCustomerID      string
	ProviderSetupIntentID   string
	ProviderPaymentMethodID string
	ProviderSubscriptionID  string
	ProviderLivemode        *bool
	ExpiresAt               time.Time
	CleanupClaimedUntil     *time.Time
	FinalizedAt             *time.Time
	CreatedAt               time.Time
	UpdatedAt               time.Time
}

WorkspaceCreationAttempt is the non-sensitive, durable state of one pre-tenant create flow. Provider ids never cross the public workspace API; the service returns only the client secret needed by the owning browser.

Jump to

Keyboard shortcuts

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