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
- Variables
- func CRName(tenant, app string) string
- func CanAddMember(plan string, currentMembers int) bool
- func CanTransitionDeploy(from, to string) bool
- func CheckOwnership(ctx context.Context, pool *pgxpool.Pool) error
- func DeployStatusStartsExecution(status string) bool
- func DeployStatuses() []string
- func IsOpenDeployStatus(status string) bool
- func IsTerminalDeployStatus(status string) bool
- func ManagedAppID(labels map[string]string) string
- func MapError(err error) error
- func Migrate(uri string) error
- func NormalizePlan(plan string) (string, error)
- func NormalizeResourceKind(kind string) string
- func RedactRepoURL(v string) string
- func RenderDeployStatus(status string) string
- func RoleAllowedOnPlan(plan, role string) bool
- func SandboxNamespace(workspaceID string) string
- func TailFeed[T any](ctx context.Context, cur *FeedCursor, pass FeedPass[T]) error
- func ValidAppName(v string) bool
- func ValidGitRef(v string) bool
- func ValidGlob(v string) bool
- func ValidImage(v string) bool
- func ValidRepo(v string) bool
- func ValidRootDir(v string) bool
- func ValidatePushNotification(notification PushNotification) error
- func WorkspaceNamespace(workspaceID string) string
- type API
- type ActivePushSubscription
- type AgentSession
- type AgentSessionListQuery
- type AgentSessionTranscriptPart
- type AgentSessionTurn
- type App
- type ArchivedFilter
- type AuditFilter
- type AuditRow
- type AutoDeployFilter
- type BillingAdmin
- type BillingEnforcement
- type BillingExportIssue
- type BillingExportReport
- type BillingExportRow
- type BillingExportStats
- type BillingLifecycle
- type BillingNotification
- type BillingOperations
- type BillingOverrideRequest
- type BillingProviderMapping
- type Blueprint
- type BlueprintSync
- type CloneSecreter
- type CommitInfo
- type CreateAppRequest
- type CreateDomainRequest
- type CreateTenantRequest
- type Deploy
- type DeployFilter
- type DeployNotification
- type DeployNotifier
- type DesiredApp
- type DevicePushSubscription
- type Disk
- type DiskUsageRow
- type Domain
- type DomainDeclaration
- type DuePushDelivery
- type DueWebhookAttempt
- type EnsureWatermark
- type Environment
- type EventFactWriter
- type FeedCursor
- type FeedPass
- type GitConnection
- type GitHubConnectTransaction
- type GroupingPair
- type GroupingStore
- type HourlyRow
- type Invite
- type Job
- type JobListFilter
- type MembershipGranter
- type NamespaceReconciler
- type NotificationSettings
- type NotifyRecipient
- type ObservedServiceState
- type PGStore
- func (s *PGStore) AbortRehydrate(ctx context.Context, id string) (AgentSession, error)
- func (s *PGStore) AcceptInviteByToken(ctx context.Context, token, subject string) (Invite, error)
- func (s *PGStore) AcceptInvitesForEmail(ctx context.Context, email, subject string) ([]Invite, error)
- func (s *PGStore) AcceptPushDelivery(ctx context.Context, delivery DuePushDelivery, ticketID string, ...) (bool, error)
- func (s *PGStore) AddDomain(ctx context.Context, appID, host, redirectForName string) error
- func (s *PGStore) AddDomainClaim(ctx context.Context, appID, host, redirectForName string) (Domain, bool, error)
- func (s *PGStore) AddMember(ctx context.Context, subject, tenantID, role string) error
- func (s *PGStore) AgentSessionSSHActivity(ctx context.Context, resourceID string, freshSince time.Time) (hasFreshOpen bool, lastEnded *time.Time, err error)
- func (s *PGStore) AgentSessionTranscript(ctx context.Context, sessionID string, afterSeq int64, maxBytes int64, ...) ([]AgentSessionTranscriptPart, error)
- func (s *PGStore) AgentSessionTranscriptBytes(ctx context.Context, sessionID string) (int64, error)
- func (s *PGStore) AgentSessionTranscriptMaxSeq(ctx context.Context, sessionID string) (int64, bool, error)
- func (s *PGStore) AgentSessionTranscriptTurnMaxIndex(ctx context.Context, sessionID string, turn int) (int64, bool, error)
- func (s *PGStore) AgentSessionTurns(ctx context.Context, sessionID string) ([]AgentSessionTurn, error)
- func (s *PGStore) AppendAgentSessionTranscript(ctx context.Context, sessionID string, parts []AgentSessionTranscriptPart) error
- func (s *PGStore) BackfillAppType(ctx context.Context, id, serviceType string) (bool, error)
- func (s *PGStore) BeginAgentSessionTurn(ctx context.Context, id, prompt, deliveryMode, phase, status string) (AgentSession, error)
- func (s *PGStore) BeginRehydrate(ctx context.Context, id, prompt, deliveryMode string) (AgentSession, error)
- func (s *PGStore) BillingExportReport(ctx context.Context, workspaceID string, start, end time.Time) (BillingExportReport, error)
- func (s *PGStore) BillingExportStats(ctx context.Context, floor, sealBefore, now time.Time, ...) (BillingExportStats, error)
- func (s *PGStore) BindClient(ctx context.Context, clientID, tenantID string) error
- func (s *PGStore) BumpOAuthRevocation(ctx context.Context, subject, clientID string) error
- func (s *PGStore) CheckBillingMutationAllowed(ctx context.Context, workspaceID string) error
- func (s *PGStore) ClaimAgentSessionForHibernation(ctx context.Context, id string) (AgentSession, error)
- func (s *PGStore) ClaimBillingNotifications(ctx context.Context, now time.Time, lease time.Duration, limit int) ([]BillingNotification, error)
- func (s *PGStore) ClaimDueBillingLifecycle(ctx context.Context, now time.Time, lease time.Duration) (BillingLifecycle, bool, error)
- func (s *PGStore) ClaimDuePushDeliveries(ctx context.Context, now, leaseUntil time.Time, limit int) ([]DuePushDelivery, error)
- func (s *PGStore) ClaimDuePushReceipts(ctx context.Context, now, leaseUntil time.Time, limit int) ([]DuePushDelivery, error)
- func (s *PGStore) ClaimDueWebhookAttempts(ctx context.Context, now, leaseUntil time.Time, limit int) ([]DueWebhookAttempt, error)
- func (s *PGStore) ClaimGitWebhookDelivery(ctx context.Context, digest string) (bool, error)
- func (s *PGStore) ClaimRoleReconciliations(ctx context.Context, limit int) ([]RoleReconciliation, error)
- func (s *PGStore) ClaimShellNonce(ctx context.Context, nonce string, expiresAt time.Time) (bool, error)
- func (s *PGStore) ClaimWebhookFailureNotice(ctx context.Context, endpointID string, now, threshold time.Time) (bool, error)
- func (s *PGStore) ClearAgentSessionSandbox(ctx context.Context, id string) error
- func (s *PGStore) CloseDeploy(ctx context.Context, id, status, resolvedImage string) (bool, error)
- func (s *PGStore) CompactUsage(ctx context.Context, before time.Time) (UsageCompaction, error)
- func (s *PGStore) CompleteAgentSessionTurn(ctx context.Context, sessionID string, turn int, complete, truncated bool, ...) error
- func (s *PGStore) CompleteBillingLifecycleWork(ctx context.Context, workspaceID string, expectedVersion int64, status string, ...) (BillingLifecycle, error)
- func (s *PGStore) CompleteBillingNotification(ctx context.Context, workspaceID string, version int64, at time.Time) error
- func (s *PGStore) CompletePushDelivery(ctx context.Context, delivery DuePushDelivery, at time.Time) (bool, error)
- func (s *PGStore) CompleteRoleReconciliation(ctx context.Context, tenantID, subject, role string) error
- func (s *PGStore) CompleteWebhookAttempt(ctx context.Context, completion WebhookAttemptCompletion) (bool, error)
- func (s *PGStore) ConsumeGitHubConnectTransaction(ctx context.Context, nonce string) (GitHubConnectTransaction, error)
- func (s *PGStore) CountAppsForTenant(ctx context.Context, tenantID string) (int, error)
- func (s *PGStore) CountGitConnections(ctx context.Context, workspaceID string) (int, error)
- func (s *PGStore) CountInvites(ctx context.Context, tenantID string) (int, error)
- func (s *PGStore) CountLiveAgentSessionSandboxes(ctx context.Context, workspaceID string, phases []string) (int, error)
- func (s *PGStore) CountPinnedAgentSessions(ctx context.Context, workspaceID string) (int, error)
- func (s *PGStore) CountRegistryCredentials(ctx context.Context, workspaceID string) (int, error)
- func (s *PGStore) CountTenantAdmins(ctx context.Context, tenantID string) (int, error)
- func (s *PGStore) CountTenantMembers(ctx context.Context, tenantID string) (int, error)
- func (s *PGStore) CountUnreadPushNotifications(ctx context.Context, tenantID, subject string) (int64, error)
- func (s *PGStore) CountWorkspaceDomainClaims(ctx context.Context, workspaceID string) (int, error)
- func (s *PGStore) CountWorkspaceGroupings(ctx context.Context, tenantID string) (int, int, error)
- func (s *PGStore) CountWorkspacesForSubjectPlan(ctx context.Context, subject, plan string) (int, error)
- func (s *PGStore) CreateAgentSession(ctx context.Context, in AgentSession) (AgentSession, error)
- func (s *PGStore) CreateApp(ctx context.Context, a App) (App, error)
- func (s *PGStore) CreateDeploy(ctx context.Context, appID, trigger, image string, generation int64, ...) (Deploy, error)
- func (s *PGStore) CreateDisk(ctx context.Context, tenantID, appID, name, mountPath string, sizeGB int32) (Disk, error)
- func (s *PGStore) CreateDomain(ctx context.Context, appID, host string, primary bool) (Domain, error)
- func (s *PGStore) CreateEnvironment(ctx context.Context, projectID, tenantID, name string) (Environment, error)
- func (s *PGStore) CreateGitHubConnectTransaction(ctx context.Context, t GitHubConnectTransaction) error
- func (s *PGStore) CreateInvite(ctx context.Context, tenantID, email, role, token, invitedBy string, ...) (Invite, error)
- func (s *PGStore) CreateJob(ctx context.Context, serviceName, tenantID, startCommand, planID string) (Job, error)
- func (s *PGStore) CreateProject(ctx context.Context, tenantID, name string) (Project, error)
- func (s *PGStore) CreateRegistryCredential(ctx context.Context, workspaceID, name, host, username, createdBy string, ...) (RegistryCredential, error)
- func (s *PGStore) CreateRollbackDeploy(ctx context.Context, appID, image, rollbackOf string, generation int64, ...) (Deploy, error)
- func (s *PGStore) CreateSSHKey(ctx context.Context, key SSHKey) (SSHKey, error)
- func (s *PGStore) CreateTenant(ctx context.Context, name, plan string) (Tenant, error)
- func (s *PGStore) CreateTenantWithMember(ctx context.Context, identityID, plan string) (Tenant, error)
- func (s *PGStore) CreateWebhookEndpoint(ctx context.Context, tenantID, name, url, secret string, eventTypes []string, ...) (WebhookEndpoint, error)
- func (s *PGStore) CreateWorkspace(ctx context.Context, name, plan, ownerSubject string) (Tenant, error)
- func (s *PGStore) CurrentUsageCoverage(ctx context.Context, workspaceID string, now time.Time) (UsageCoverage, error)
- func (s *PGStore) DeleteAgentSession(ctx context.Context, id string) error
- func (s *PGStore) DeleteApp(ctx context.Context, id string) error
- func (s *PGStore) DeleteDisk(ctx context.Context, id string) error
- func (s *PGStore) DeleteDomain(ctx context.Context, appID, host string) error
- func (s *PGStore) DeleteEnvironment(ctx context.Context, id string) error
- func (s *PGStore) DeleteGitConnection(ctx context.Context, workspaceID string, installationID int64) error
- func (s *PGStore) DeleteInvite(ctx context.Context, tenantID, id string) error
- func (s *PGStore) DeleteProject(ctx context.Context, id string) error
- func (s *PGStore) DeleteRegistryCredential(ctx context.Context, workspaceID, id string) error
- func (s *PGStore) DeleteSSHKey(ctx context.Context, subject, id string) error
- func (s *PGStore) DeleteTenant(ctx context.Context, id string) error
- func (s *PGStore) DeleteWebhookEndpoint(ctx context.Context, tenantID, id string) error
- func (s *PGStore) DisconnectBlueprint(ctx context.Context, id, tenantID string) error
- func (s *PGStore) DiskUsageForWindow(ctx context.Context, from, to time.Time) ([]DiskUsageRow, error)
- func (s *PGStore) EndSSHSession(ctx context.Context, sessionID, result string, endedAt time.Time) error
- func (s *PGStore) EnqueuePushNotifications(ctx context.Context, items []PushNotificationBatchItem, at time.Time, ...) error
- func (s *PGStore) EnqueueWebhookDeliveries(ctx context.Context, deliveries []WebhookDelivery, at time.Time, key string, ...) (WebhookEnqueueResult, error)
- func (s *PGStore) EnsureBillingEnforcement(ctx context.Context, e BillingEnforcement) (BillingEnforcement, error)
- func (s *PGStore) EnsureBillingLifecycle(ctx context.Context, workspaceID string) (BillingLifecycle, error)
- func (s *PGStore) EnsurePushWatermark(ctx context.Context, at time.Time) (time.Time, string, error)
- func (s *PGStore) EnsureWebhookWatermark(ctx context.Context, at time.Time) (time.Time, string, error)
- func (s *PGStore) ExpireHibernatedAgentSession(ctx context.Context, id, snapshotRef string) (AgentSession, error)
- func (s *PGStore) ExtendBillingGrace(ctx context.Context, workspaceID string, extension time.Duration, ...) (BillingLifecycle, error)
- func (s *PGStore) FailBillingLifecycleWork(ctx context.Context, workspaceID string, expectedVersion int64, message string, ...) error
- func (s *PGStore) FailBillingNotification(ctx context.Context, workspaceID string, version int64, message string, ...) error
- func (s *PGStore) FailRoleReconciliation(ctx context.Context, tenantID, subject, role, message string) error
- func (s *PGStore) FinalizeAgentSession(ctx context.Context, id, phase, headSHA, prURL string, prNumber int, ...) (AgentSession, error)
- func (s *PGStore) ForceBillingRecovery(ctx context.Context, workspaceID, actor, reason string, at time.Time) (BillingLifecycle, error)
- func (s *PGStore) GetAgentSession(ctx context.Context, id string) (AgentSession, error)
- func (s *PGStore) GetApp(ctx context.Context, id string) (App, error)
- func (s *PGStore) GetAppProtectedStatus(ctx context.Context, appID string) (string, error)
- func (s *PGStore) GetBillingLifecycle(ctx context.Context, workspaceID string) (BillingLifecycle, error)
- func (s *PGStore) GetBlueprint(ctx context.Context, id, tenantID string) (Blueprint, error)
- func (s *PGStore) GetBlueprintByRepo(ctx context.Context, tenantID, repo, branch string) (Blueprint, error)
- func (s *PGStore) GetDeploy(ctx context.Context, appID, deployID string) (Deploy, error)
- func (s *PGStore) GetDisk(ctx context.Context, id string) (Disk, error)
- func (s *PGStore) GetDomainClaim(ctx context.Context, appID, host string) (Domain, error)
- func (s *PGStore) GetEnvironment(ctx context.Context, id string) (Environment, error)
- func (s *PGStore) GetEnvironmentProtectedStatus(ctx context.Context, environmentID string) (string, error)
- func (s *PGStore) GetGitConnection(ctx context.Context, workspaceID string) (GitConnection, error)
- func (s *PGStore) GetGitConnectionByOwner(ctx context.Context, workspaceID, accountLogin string) (GitConnection, error)
- func (s *PGStore) GetInvite(ctx context.Context, tenantID, id string) (Invite, error)
- func (s *PGStore) GetJob(ctx context.Context, serviceName, tenantID, jobID string) (Job, error)
- func (s *PGStore) GetNotificationSettings(ctx context.Context, tenantID, subject string) (NotificationSettings, error)
- func (s *PGStore) GetProject(ctx context.Context, id string) (Project, error)
- func (s *PGStore) GetRegistryCredential(ctx context.Context, workspaceID, id string) (RegistryCredential, error)
- func (s *PGStore) GetRegistryCredentialByHost(ctx context.Context, workspaceID, host string) (RegistryCredential, error)
- func (s *PGStore) GetRegistryCredentialByID(ctx context.Context, id string) (RegistryCredential, error)
- func (s *PGStore) GetRegistryCredentialsByIDs(ctx context.Context, workspaceID string, ids []string) ([]RegistryCredential, error)
- func (s *PGStore) GetServiceEvent(ctx context.Context, workspaceID, eventID string) (ServiceEventLookup, error)
- func (s *PGStore) GetTenant(ctx context.Context, id string) (Tenant, error)
- func (s *PGStore) GetTenantMember(ctx context.Context, tenantID, subject string) (TenantMember, error)
- func (s *PGStore) GetWebhookEndpoint(ctx context.Context, tenantID, id string) (WebhookEndpoint, error)
- func (s *PGStore) GitConnectionByInstallation(ctx context.Context, installationID int64) (GitConnection, error)
- func (s *PGStore) HasPendingRoleReconciliation(ctx context.Context, tenantID, subject string) (bool, error)
- func (s *PGStore) HibernateAgentSession(ctx context.Context, id, snapshotRef string, snapshotBytes int64, ...) (AgentSession, error)
- func (s *PGStore) IdempotentCLIRefresh(ctx context.Context, tokenHash [sha256.Size]byte, ttl time.Duration, ...) ([]byte, int, error)
- func (s *PGStore) InsertBlueprintSync(ctx context.Context, run BlueprintSync) (BlueprintSync, error)
- func (s *PGStore) InsertServiceEventFact(ctx context.Context, fact ServiceEventFact) (bool, error)
- func (s *PGStore) InsertServiceEventFacts(ctx context.Context, facts []ServiceEventFact) error
- func (s *PGStore) IsMember(ctx context.Context, subject, tenantID string) (bool, error)
- func (s *PGStore) LastHealthyTransitionAt(ctx context.Context, appID string) (time.Time, error)
- func (s *PGStore) LatestUsageWindow(ctx context.Context, resourceKind, serviceID, kind string) (time.Time, error)
- func (s *PGStore) LatestUsageWindowForKind(ctx context.Context, kind string) (time.Time, bool, error)
- func (s *PGStore) ListActiveBillingEnforcements(ctx context.Context, workspaceID string) ([]BillingEnforcement, error)
- func (s *PGStore) ListActivePushSubscriptions(ctx context.Context) ([]ActivePushSubscription, error)
- func (s *PGStore) ListAgentSessions(ctx context.Context, workspaceID string, q AgentSessionListQuery) ([]AgentSession, error)
- func (s *PGStore) ListAgentSessionsByPhases(ctx context.Context, phases []string) ([]AgentSession, error)
- func (s *PGStore) ListApps(ctx context.Context) ([]App, error)
- func (s *PGStore) ListAuditEvents(ctx context.Context, workspaceID string, filter AuditFilter) ([]AuditRow, error)
- func (s *PGStore) ListBillingExportIssues(ctx context.Context, openOnly bool, limit int) ([]BillingExportIssue, error)
- func (s *PGStore) ListBillingOwnerSubjects(ctx context.Context, workspaceID string) ([]string, error)
- func (s *PGStore) ListBillingProviderMappings(ctx context.Context, livemode bool, limit int) ([]BillingProviderMapping, error)
- func (s *PGStore) ListBlueprintSyncs(ctx context.Context, blueprintID, cursor string, limit int) ([]BlueprintSync, error)
- func (s *PGStore) ListBlueprints(ctx context.Context, tenantID string) ([]Blueprint, error)
- func (s *PGStore) ListDeploys(ctx context.Context, appID string, filter DeployFilter) ([]Deploy, error)
- func (s *PGStore) ListDesiredApps(ctx context.Context) ([]DesiredApp, error)
- func (s *PGStore) ListDisks(ctx context.Context, tenantID, appID string) ([]Disk, error)
- func (s *PGStore) ListDomainClaims(ctx context.Context, appID string) ([]Domain, error)
- func (s *PGStore) ListEnabledWebhookEndpoints(ctx context.Context) ([]WebhookEndpoint, error)
- func (s *PGStore) ListEnvironmentServices(ctx context.Context, environmentID, projectID string) ([]string, error)
- func (s *PGStore) ListEnvironments(ctx context.Context, projectID string) ([]Environment, error)
- func (s *PGStore) ListGitConnections(ctx context.Context, workspaceID string) ([]GitConnection, error)
- func (s *PGStore) ListHibernatedForRetention(ctx context.Context, now time.Time, limit int) ([]AgentSession, error)
- func (s *PGStore) ListInvites(ctx context.Context, tenantID string) ([]Invite, error)
- func (s *PGStore) ListJobs(ctx context.Context, serviceName, tenantID string, filter JobListFilter) ([]Job, error)
- func (s *PGStore) ListNotifyRecipients(ctx context.Context, tenantID string) ([]NotifyRecipient, error)
- func (s *PGStore) ListOpenDeploys(ctx context.Context) ([]Deploy, error)
- func (s *PGStore) ListOwnDevicePushSubscriptions(ctx context.Context, tenantID, subject string) ([]DevicePushSubscription, error)
- func (s *PGStore) ListOwnPushNotifications(ctx context.Context, tenantID, subject string, limit int) ([]PushNotification, error)
- func (s *PGStore) ListOwnWebPushSubscriptions(ctx context.Context, tenantID, subject string) ([]WebPushSubscription, error)
- func (s *PGStore) ListProjectServices(ctx context.Context, projectID string) ([]string, error)
- func (s *PGStore) ListProjects(ctx context.Context, tenantID string) ([]Project, error)
- func (s *PGStore) ListRegistryCredentials(ctx context.Context, workspaceID string) ([]RegistryCredential, error)
- func (s *PGStore) ListSSHKeys(ctx context.Context, subject string) ([]SSHKey, error)
- func (s *PGStore) ListSandboxTenantKeys(ctx context.Context) ([]SandboxTenantKey, error)
- func (s *PGStore) ListServiceEvents(ctx context.Context, appID, target, ownerWorkspace string, ...) ([]ServiceEventRow, error)
- func (s *PGStore) ListTenantMembers(ctx context.Context, tenantID string) ([]TenantMember, error)
- func (s *PGStore) ListTenants(ctx context.Context) ([]Tenant, error)
- func (s *PGStore) ListTenantsForSubject(ctx context.Context, subject string) ([]Tenant, error)
- func (s *PGStore) ListTerminalAgentSessionsForPush(ctx context.Context, since time.Time) ([]AgentSession, error)
- func (s *PGStore) ListTerminalAgentSessionsWithSandbox(ctx context.Context, since time.Time) ([]AgentSession, error)
- func (s *PGStore) ListWebhookAttempts(ctx context.Context, filter WebhookAttemptFilter) ([]WebhookAttempt, error)
- func (s *PGStore) ListWebhookEndpoints(ctx context.Context, tenantIDs []string, afterAt time.Time, afterKey string, ...) ([]WebhookEndpoint, error)
- func (s *PGStore) ListWebhookEvents(ctx context.Context, afterAt time.Time, afterKey string, until time.Time, ...) ([]WebhookEventRow, error)
- func (s *PGStore) ListWorkspaceEnvironmentServices(ctx context.Context, tenantID string) (map[string][]string, error)
- func (s *PGStore) ListWorkspaceEnvironments(ctx context.Context, tenantID string) ([]Environment, error)
- func (s *PGStore) MarkBillingEnforcementRecovered(ctx context.Context, workspaceID, kind, name string, at time.Time) error
- func (s *PGStore) MarkOwnPushNotificationRead(ctx context.Context, tenantID, subject, eventID string, at time.Time) (bool, error)
- func (s *PGStore) MarkUsageAttempted(ctx context.Context, attempts []UsageExportAttempt, at time.Time) error
- func (s *PGStore) MarkUsageEmitted(ctx context.Context, rows []HourlyRow, at time.Time) error
- func (s *PGStore) OAuthRevokedAt(ctx context.Context, subject, clientID string) (time.Time, bool, error)
- func (s *PGStore) ObserveSandboxMeter(ctx context.Context, obs SandboxMeterObservation) error
- func (s *PGStore) OwnerIDForSubject(ctx context.Context, subject string) (string, error)
- func (s *PGStore) PaymentEligibility(ctx context.Context, workspaceID string) (PaymentEligibility, error)
- func (s *PGStore) PaymentMethodBound(ctx context.Context, workspaceID string) (bool, error)
- func (s *PGStore) Ping(ctx context.Context) error
- func (s *PGStore) PromoteDomainClaim(ctx context.Context, appID, id, expectedChallenge string, at time.Time) (Domain, error)
- func (s *PGStore) PurgeAuditEvents(ctx context.Context, before time.Time) (int64, error)
- func (s *PGStore) PurgeSSHSessions(ctx context.Context, before time.Time) (int64, error)
- func (s *PGStore) PurgeStripeBillingEvents(ctx context.Context, before time.Time) (int64, error)
- func (s *PGStore) PushDeliveryStats(ctx context.Context) (PushQueueStats, error)
- func (s *PGStore) QuarantineOldUsageAttempts(ctx context.Context, before, at time.Time) (int64, error)
- func (s *PGStore) QueueWebhookResend(ctx context.Context, request WebhookResendRequest) (WebhookAttempt, error)
- func (s *PGStore) ReclaimEmptyBlueprintGroupings(ctx context.Context, tenantID string, pairs []GroupingPair, ...) (removedEnvironments, removedProjects []string, err error)
- func (s *PGStore) ReconcileUsageSourceStreams(ctx context.Context, active []UsageResourceRef, through time.Time) error
- func (s *PGStore) Record(ctx context.Context, ev core.AuditEvent) error
- func (s *PGStore) RecordAgentSessionDispatch(ctx context.Context, id, sandboxID, phase, status, deliveryMode string) (AgentSession, error)
- func (s *PGStore) RecordDomainVerificationAttempt(ctx context.Context, appID, id string, at time.Time) error
- func (s *PGStore) RecordObservedServiceState(ctx context.Context, obs ObservedServiceState) ([]ServiceEventFact, error)
- func (s *PGStore) RecordPushReceipt(ctx context.Context, d DuePushDelivery, code string, at, next time.Time, ...) (bool, error)
- func (s *PGStore) RecordPushSendFailure(ctx context.Context, delivery DuePushDelivery, code string, at, next time.Time, ...) (bool, error)
- func (s *PGStore) RecordStripeBillingEvent(ctx context.Context, e StripeBillingEvent, grace time.Duration) (BillingLifecycle, bool, bool, error)
- func (s *PGStore) RecordUsageExportResult(ctx context.Context, accepted []UsageExportAttempt, ...) error
- func (s *PGStore) RecordUsageSourceHealth(ctx context.Context, records []UsageSourceRecord) error
- func (s *PGStore) RefreshInvite(ctx context.Context, tenantID, id, token string, expiresAt time.Time) (Invite, error)
- func (s *PGStore) RehydrateAgentSession(ctx context.Context, id, sandboxID, phase, status, deliveryMode string) (AgentSession, error)
- func (s *PGStore) ReleaseGitWebhookDelivery(ctx context.Context, digest string) error
- func (s *PGStore) ReleasePushDelivery(ctx context.Context, delivery DuePushDelivery) (bool, error)
- func (s *PGStore) RemoveDomain(ctx context.Context, appID, host string) error
- func (s *PGStore) RemoveMember(ctx context.Context, tenantID, subject string) error
- func (s *PGStore) RenameEnvironment(ctx context.Context, id, name string) error
- func (s *PGStore) RenameProject(ctx context.Context, id, name string) error
- func (s *PGStore) RenameTenant(ctx context.Context, id, name string) (Tenant, error)
- func (s *PGStore) ReplaceDomainClaims(ctx context.Context, appID string, declarations []DomainDeclaration) ([]Domain, error)
- func (s *PGStore) ReplaceDomains(ctx context.Context, appID, primary string, hosts []string) error
- func (s *PGStore) ResolveBillingExportIssue(ctx context.Context, transactionID, action, actor, reason string, at time.Time) (BillingExportIssue, error)
- func (s *PGStore) RevokeAllDevicePushSubscriptions(ctx context.Context, tenantID, subject string) (int64, error)
- func (s *PGStore) RevokeAllWebPushSubscriptions(ctx context.Context, tenantID, subject string) (int64, error)
- func (s *PGStore) RevokeDevicePushSubscription(ctx context.Context, tenantID, subject, deviceID string) (bool, error)
- func (s *PGStore) RevokeExactPushSubscription(ctx context.Context, d DuePushDelivery) (bool, error)
- func (s *PGStore) RevokeWebPushSubscription(ctx context.Context, tenantID, subject, browserID string) (bool, error)
- func (s *PGStore) RunGroupingTx(ctx context.Context, fn func(GroupingStore) error) error
- func (s *PGStore) SSHKeyByFingerprint(ctx context.Context, fingerprint string) (SSHKey, error)
- func (s *PGStore) SandboxKeyForWorkspace(ctx context.Context, workspaceID string) (string, error)
- func (s *PGStore) SandboxKeyLookup(ctx context.Context, workspaceID string) (string, bool, error)
- func (s *PGStore) SelectUnemittedUsage(ctx context.Context, floor, sealBefore time.Time, limit int, ...) ([]HourlyRow, error)
- func (s *PGStore) SetAgentSessionArchived(ctx context.Context, id string, archived bool) (AgentSession, error)
- func (s *PGStore) SetAgentSessionFailure(ctx context.Context, id, sandboxID, reason string) (AgentSession, error)
- func (s *PGStore) SetAgentSessionLifecycle(ctx context.Context, id, sandboxID, phase, status string, canceled bool) (AgentSession, error)
- func (s *PGStore) SetAgentSessionPinned(ctx context.Context, id string, pinned bool, retainUntil *time.Time) (AgentSession, error)
- func (s *PGStore) SetAppDisplayName(ctx context.Context, id string, displayName string) error
- func (s *PGStore) SetAppEnvironment(ctx context.Context, id, projectID, environmentID string) error
- func (s *PGStore) SetAppIdleTTL(ctx context.Context, id string, seconds int32) error
- func (s *PGStore) SetAppImage(ctx context.Context, id string, image string) error
- func (s *PGStore) SetAppReplicas(ctx context.Context, id string, replicas int32) error
- func (s *PGStore) SetAppSource(ctx context.Context, id, repo, image, branch string, ...) error
- func (s *PGStore) SetAppSuspended(ctx context.Context, id string, suspended bool) error
- func (s *PGStore) SetAppTier(ctx context.Context, id string, tier string) error
- func (s *PGStore) SetBillingException(ctx context.Context, workspaceID, exception string, enabled bool, ...) (bool, BillingLifecycle, error)
- func (s *PGStore) SetDeployPreDeployStatus(ctx context.Context, id, status string) (bool, error)
- func (s *PGStore) SetEnvironmentACL(ctx context.Context, id, protectedStatus string, networkIsolationEnabled bool, ...) error
- func (s *PGStore) SetEnvironmentServices(ctx context.Context, environmentID, projectID, tenantID string, ...) error
- func (s *PGStore) SetPaymentMethodBound(ctx context.Context, workspaceID string, at time.Time) error
- func (s *PGStore) SetProjectServices(ctx context.Context, projectID, tenantID string, serviceIDs []string) ([]string, error)
- func (s *PGStore) SetTenantBillingExcluded(ctx context.Context, tenantID string, excluded bool, actor string, ...) (bool, error)
- func (s *PGStore) SetWebhookEndpointEnabled(ctx context.Context, tenantID, id string, enabled bool, reason string) (WebhookEndpoint, error)
- func (s *PGStore) StartSSHSession(ctx context.Context, session SSHSessionAudit) error
- func (s *PGStore) SubjectIsWorkspaceAdmin(ctx context.Context, tenantID, subject string) (bool, error)
- func (s *PGStore) SweepPushRetention(ctx context.Context, revokedBefore, terminalBefore time.Time) (PushSweepResult, error)
- func (s *PGStore) SweepWebhookDeliveries(ctx context.Context, before time.Time, keepPerEndpoint, limit int) (int64, error)
- func (s *PGStore) TenantForIdentity(ctx context.Context, subject string) (Tenant, error)
- func (s *PGStore) TenantForOwner(ctx context.Context, identityID string) (Tenant, error)
- func (s *PGStore) TerminateMissingSandboxMeters(ctx context.Context, workspaceID string, seen []string, observedAt time.Time) error
- func (s *PGStore) TouchBillingProviderMapping(ctx context.Context, workspaceID string, at time.Time) error
- func (s *PGStore) TouchRegistryCredential(ctx context.Context, workspaceID, id string) error
- func (s *PGStore) TransitionDeploy(ctx context.Context, ...) (bool, error)
- func (s *PGStore) UnbindClient(ctx context.Context, clientID string) error
- func (s *PGStore) UpdateBlueprint(ctx context.Context, id, tenantID string, name *string, autoSync *bool, ...) (Blueprint, error)
- func (s *PGStore) UpdateBlueprintSync(ctx context.Context, id, state string, completedAt *time.Time, errMsg *string) (BlueprintSync, error)
- func (s *PGStore) UpdateDisk(ctx context.Context, id string, name, mountPath *string, sizeGB *int32) (Disk, error)
- func (s *PGStore) UpdateJobStatus(ctx context.Context, jobID, status string) (Job, error)
- func (s *PGStore) UpdateMemberRole(ctx context.Context, tenantID, subject, role string) error
- func (s *PGStore) UpdateRegistryCredential(ctx context.Context, workspaceID, id, name, username string, ...) (RegistryCredential, error)
- func (s *PGStore) UpdateTenantPlan(ctx context.Context, id, plan string) (Tenant, error)
- func (s *PGStore) UpdateWebhookEndpoint(ctx context.Context, tenantID, id, name, url string, eventTypes []string, ...) (WebhookEndpoint, error)
- func (s *PGStore) UpsertBillingProviderMapping(ctx context.Context, m BillingProviderMapping) error
- func (s *PGStore) UpsertBlueprint(ctx context.Context, b Blueprint) (Blueprint, error)
- func (s *PGStore) UpsertDevicePushSubscription(ctx context.Context, sub DevicePushSubscription) (DevicePushSubscription, error)
- func (s *PGStore) UpsertGitConnection(ctx context.Context, c GitConnection) (GitConnection, error)
- func (s *PGStore) UpsertNotificationPushPolicy(ctx context.Context, tenantID, subject string, policy json.RawMessage) (NotificationSettings, error)
- func (s *PGStore) UpsertNotificationSettings(ctx context.Context, tenantID, subject string, ...) (NotificationSettings, error)
- func (s *PGStore) UpsertUsageHourly(ctx context.Context, row HourlyRow) error
- func (s *PGStore) UpsertWebPushSubscription(ctx context.Context, sub WebPushSubscription) (WebPushSubscription, error)
- func (s *PGStore) UsageMonthToDate(ctx context.Context, workspaceID string, now time.Time) ([]UsageSummaryRow, error)
- func (s *PGStore) WithTenantAdvisoryLock(ctx context.Context, tenantID string, fn func() error) error
- func (s *PGStore) WorkspaceForSandboxKey(ctx context.Context, apiKey string) (string, error)
- type PaymentEligibility
- type PlanLimits
- type Project
- type PushNotification
- type PushNotificationBatchItem
- type PushQueueStats
- type PushSweepResult
- type QuotaCaps
- type Reconciler
- type ReconcilerMetrics
- type RegistryCredential
- type ResolveBillingExportIssueRequest
- type RoleReconciliation
- type SSHKey
- type SSHSessionAudit
- type SandboxMeterObservation
- type SandboxTenantKey
- type SandboxTenantResolver
- type ServiceEventFact
- type ServiceEventFactType
- type ServiceEventFilter
- type ServiceEventLookup
- type ServiceEventRow
- type SetBillingExcludedRequest
- type Store
- type StripeBillingEvent
- type Tenant
- type TenantMember
- type UsageCompaction
- type UsageCoverage
- type UsageExportAttempt
- type UsageExportReject
- type UsageResourceRef
- type UsageSourceObservation
- type UsageSourceRecord
- type UsageSummaryRow
- type WebPushSubscription
- type WebhookAttempt
- type WebhookAttemptCompletion
- type WebhookAttemptFilter
- type WebhookDelivery
- type WebhookEndpoint
- type WebhookEnqueueResult
- type WebhookEventRow
- type WebhookResendRequest
Constants ¶
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).
const ( BillingHealthy = "healthy" BillingGrace = "grace" BillingEnforcing = "enforcing" BillingEnforced = "enforced" BillingRecovering = "recovering" BillingExcluded = "excluded" BillingComped = "comped" BillingOutcomeFailure = "failure" BillingOutcomeSuccess = "success" )
const ( BlueprintSyncStateCreated = "created" BlueprintSyncStateRunning = "running" BlueprintSyncStateSuccess = "success" BlueprintSyncStateError = "error" )
Blueprint sync state constants (Render's vocabulary).
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.
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 )
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).
const ( EventReasonImagePullBackoff = "image_pull_backoff" EventReasonReadinessFailed = "readiness_failed" EventReasonRootDirectory = "root_directory" EventReasonBuildFilter = "build_filter" EventReasonSkipPhrase = "skip_phrase" )
const ( EventSourceDeploy = "deploy" EventSourceAudit = "audit" EventSourceFact = "fact" )
Event source discriminators — which table an event row was projected from.
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.
const ( JobPending = "pending" JobRunning = "running" JobSucceeded = "succeeded" JobFailed = "failed" JobCanceled = "canceled" )
Job status constants — the Render job status enum.
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.
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).
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.
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.
const ( PreDeployRunning = "running" PreDeploySucceeded = "succeeded" PreDeployFailed = "failed" )
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.
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.
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.
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" )
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.
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.
const ( WebhookAttemptPending = "pending" WebhookAttemptDelivered = "delivered" WebhookAttemptFailed = "failed" WebhookAttemptAutomatic = "automatic" WebhookAttemptManual = "manual" )
const ( MaxActiveWebPushBrowsersPerSubject = 10 MaxActiveWebPushBrowsersPerWorkspace = 1000 )
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.
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.
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.
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).
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.
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.
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.
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.
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 ¶
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) )
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) )
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.
var ( ErrNotFound = errors.New("not found") ErrConflict = errors.New("already exists") ErrInvalid = errors.New("invalid") )
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).
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) )
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.
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).
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.
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.
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).
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.
var WorkspacePlans = []string{PlanHobby, PlanPro, PlanScale, PlanEnterprise}
WorkspacePlans is the catalog in ladder order (cheapest first), the source of truth for validation and any plan picker.
Functions ¶
func CRName ¶
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 ¶
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 ¶
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 CheckOwnership ¶
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 DeployStatusStartsExecution ¶
DeployStatusStartsExecution reports whether reaching status means the deploy's work has actually begun — the condition TransitionDeploy stamps started_at on. 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).
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 IsOpenDeployStatus ¶
IsOpenDeployStatus reports whether status may still advance or be canceled.
func IsTerminalDeployStatus ¶
IsTerminalDeployStatus reports whether status is a known terminal state.
func ManagedAppID ¶
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 ¶
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 ¶
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 ¶
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 ¶
NormalizeResourceKind preserves the pre-resource-kind behavior for callers that omit the field. Persisted rows always carry an explicit kind.
func RedactRepoURL ¶
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 ¶
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 RoleAllowedOnPlan ¶
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 ¶
SandboxNamespace is the sandbox-regime namespace name for a workspace id.
func TailFeed ¶
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 ValidAppName ¶
ValidAppName reports whether v is a valid App/tenant name: a DNS-1123 label of 1-30 chars. Exported so bex-api's public create verb enforces the exact same rule as this internal create API — the two can't disagree about what a valid name is (the same single-source rationale as MaxReplicas).
func ValidGitRef ¶
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 ¶
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 ValidImage ¶
ValidImage reports whether v is an acceptable prebuilt OCI image reference (host[:port]/repo[:tag][@digest], no whitespace/control/shell-meta characters, ≤512 bytes). Exported so bex-api and the internal create API enforce one rule (w1/m53). Empty is handled by the caller (repo-or-image required).
A reference whose registry HOST component (the first path segment, when it looks like a host: contains "."/":" or is "localhost") is a non-public IP literal is refused (codex round-7 F6): the kubelet pulls tenant images from the node's network context — outside every pod egress policy — so a private, loopback, link-local, CGNAT, or metadata literal turns spec.Image into a node-origin probe with tenant-visible pull-error detail. Explicit registry hosts are additionally restricted to the platform's exact trusted set. A one-time DNS lookup is insufficient because kubelet resolves later from the node network and an attacker could rebind between the two lookups.
func ValidRepo ¶
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 ¶
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 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 ¶
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.
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 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
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 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
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"`
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"`
// 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 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.
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 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.
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 = 3 )
type BillingAdmin ¶
type BillingEnforcement ¶
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 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 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"`
}
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 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"`
}
BlueprintSync is a row of `blueprint_syncs` — one recorded sync run. State machine: created → running → success | error.
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, or resolution failed. 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 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'. 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"`
}
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 ¶
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 ¶
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
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 ¶
EnsureWatermark seeds a tailer's durable watermark at `at` if none exists yet and returns the current one — EnsureWebhookWatermark / EnsurePushWatermark.
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.
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 enabled endpoint's creation). 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, ADR075). Since w5/m74 the installation id is the primary key, so a workspace may hold many connections (one per GitHub account/org it has installed the App on) while an installation still belongs to at most one workspace. A re-connect of the same installation upserts; a different installation adds a row.
type GitHubConnectTransaction ¶
type GitHubConnectTransaction struct {
Nonce string
TenantID string
Subject string
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 GroupingPair ¶
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 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.
ReadyTransitionAt time.Time
}
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 ¶
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 (*PGStore) AbortRehydrate ¶
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 ¶
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) AddDomain ¶
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 ¶
AddMember records a subject's membership in a tenant. Used both by the platform tenant-create path (store/api.go, an explicit Admin) and by BindClient (a minted API key is "membership" too — same table, same shape).
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 ¶
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) 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) BackfillAppType ¶
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) 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) BillingExportReport ¶
func (*PGStore) BillingExportStats ¶
func (*PGStore) BindClient ¶
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) BumpOAuthRevocation ¶
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) CheckBillingMutationAllowed ¶
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) 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 (*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 (*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) ClaimGitWebhookDelivery ¶
ClaimGitWebhookDelivery atomically claims a delivery-body digest. It returns true when this caller is the first to claim (the delivery should proceed) and false when the body was already processed (a replay: answer success, mutate nothing). The INSERT .. ON CONFLICT DO NOTHING is the atomicity boundary — concurrent duplicates of one delivery collapse to exactly one mutation.
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) 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) ClearAgentSessionSandbox ¶
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) CloseDeploy ¶
func (*PGStore) CompactUsage ¶
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) CompleteBillingLifecycleWork ¶
func (*PGStore) CompleteBillingNotification ¶
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) 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 ¶
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) CountGitConnections ¶
CountGitConnections returns how many connections a workspace holds — the per-workspace quota check (BEX_MAX_GIT_CONNECTIONS_PER_WORKSPACE, ADR075 §2).
func (*PGStore) CountInvites ¶
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 ¶
CountPinnedAgentSessions counts a workspace's pinned sessions — the ADR059 D5 pin-quota dimension. Best-effort like the live cap; no lock.
func (*PGStore) CountRegistryCredentials ¶
CountRegistryCredentials returns how many stored registry credentials the workspace owns. It backs the feature's per-workspace admission quota.
func (*PGStore) CountTenantAdmins ¶
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 ¶
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 (*PGStore) CountWorkspaceDomainClaims ¶
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 ¶
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 ¶
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 (*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 (*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) 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 (*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) (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 ¶
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 (*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) 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) DeleteAgentSession ¶
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) DeleteDisk ¶
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 (*PGStore) DeleteEnvironment ¶
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 ¶
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 (*PGStore) DeleteRegistryCredential ¶
DeleteRegistryCredential removes the row, scoped to workspaceID. Not-found (including a cross-workspace id) is ErrNotFound.
func (*PGStore) DeleteSSHKey ¶
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 ¶
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) DeleteWebhookEndpoint ¶
DeleteWebhookEndpoint removes an endpoint (its deliveries cascade).
func (*PGStore) DisconnectBlueprint ¶
DisconnectBlueprint marks the blueprint as disconnected (hidden from list; resources remain untouched). Returns ErrNotFound if not owned by tenantID.
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) EndSSHSession ¶
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 ¶
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) 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) ExtendBillingGrace ¶
func (*PGStore) FailBillingLifecycleWork ¶
func (*PGStore) FailBillingNotification ¶
func (*PGStore) FailRoleReconciliation ¶
func (*PGStore) FinalizeAgentSession ¶
func (s *PGStore) FinalizeAgentSession(ctx context.Context, id, phase, headSHA, prURL string, prNumber int, evidence json.RawMessage, failureReason string) (AgentSession, 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.
func (*PGStore) ForceBillingRecovery ¶
func (*PGStore) GetAgentSession ¶
func (*PGStore) GetAppProtectedStatus ¶
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 (*PGStore) GetBlueprint ¶
GetBlueprint fetches a blueprint by id, scoped to tenantID (returns ErrNotFound if the id belongs to a different workspace).
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.
func (*PGStore) GetDomainClaim ¶
GetDomainClaim returns one exact host claim owned by appID.
func (*PGStore) GetEnvironment ¶
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 ¶
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) GetInvite ¶
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) 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 (*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) 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) GitConnectionByInstallation ¶
func (s *PGStore) GitConnectionByInstallation(ctx context.Context, installationID int64) (GitConnection, error)
GitConnectionByInstallation returns the connection bound to installationID, or ErrNotFound when no workspace has connected it. It backs the unique installation->workspace binding (w1/m65 F2): because the App JWT can look up EVERY installation of itself, a GetInstallation success is existence proof only — this lookup is what lets the service reject a second workspace trying to claim an installation another already owns.
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.
func (*PGStore) InsertServiceEventFact ¶
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 ¶
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) LastHealthyTransitionAt ¶
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) 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) ListActiveBillingEnforcements ¶
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) 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) 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) ListBillingExportIssues ¶
func (*PGStore) ListBillingOwnerSubjects ¶
func (*PGStore) ListBillingProviderMappings ¶
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 ¶
ListBlueprints returns all non-disconnected blueprints for a tenant, newest first.
func (*PGStore) ListDeploys ¶
func (*PGStore) ListDesiredApps ¶
func (s *PGStore) ListDesiredApps(ctx context.Context) ([]DesiredApp, error)
func (*PGStore) ListDomainClaims ¶
ListDomainClaims returns pending and verified claims in stable projection order, primary first and then creation order.
func (*PGStore) ListEnabledWebhookEndpoints ¶
func (s *PGStore) ListEnabledWebhookEndpoints(ctx context.Context) ([]WebhookEndpoint, error)
ListEnabledWebhookEndpoints returns every enabled endpoint platform-wide for the dispatcher's subscription table, refreshed each poll pass.
Only the columns dispatch needs are selected (id, tenant, event types, created_at). 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) 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 (*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 ¶
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) ListNotifyRecipients ¶
func (s *PGStore) ListNotifyRecipients(ctx context.Context, tenantID string) ([]NotifyRecipient, error)
ListNotifyRecipients returns every member of tenantID 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 (*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 (*PGStore) ListOwnWebPushSubscriptions ¶
func (*PGStore) ListProjectServices ¶
ListProjectServices returns the public ids of all services in the project.
func (*PGStore) ListProjects ¶
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) ListSSHKeys ¶
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, target, ownerWorkspace string, f ServiceEventFilter) ([]ServiceEventRow, error)
ListServiceEvents returns one service's composed activity feed, newest first.
appID is the app's control-plane row id (deploys are keyed by it); target is core.ServiceTarget(appName) (audit rows are keyed by it) — the two sources key on different identifiers for the same service, which is why both are passed rather than derived here. ownerWorkspace is the tenant that OWNS the service: the query scopes audit rows to it (see serviceEventsQuery), so it is a parameter of the read, not an option a caller may forget to set.
func (*PGStore) ListTenantMembers ¶
ListTenantMembers returns a workspace's members, oldest first.
func (*PGStore) ListTenantsForSubject ¶
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, since time.Time) ([]AgentSession, error)
ListTerminalAgentSessionsWithSandbox returns terminal sessions (completed/ failed/canceled) that still carry a sandbox id and were updated at or after `since`, oldest first (ADR054 D6). It drives the Completer's deferred-teardown reaper: a terminal session keeps its sandbox_id only while its editor SSH is held open, so this is the small working set of sandboxes awaiting reclamation. The recency bound keeps the scan cheap and skips pre-feature history (whose sandboxes are long gone). Trusted background read — no authorization.
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) 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) MarkBillingEnforcementRecovered ¶
func (*PGStore) MarkOwnPushNotificationRead ¶
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 ¶
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) 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 ¶
OwnerIDForSubject returns the stable opaque "own-" id for a subject, minting and persisting one on first sight (w6/m7). The Render owners members surface reports userId as this id instead of leaking the raw Kratos/Hydra subject. The upsert is race-safe and idempotent: a concurrent first-sight for the same subject yields exactly one row (the unique PK is the gate), and DO UPDATE is a no-op that only lets RETURNING surface the EXISTING own_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 ¶
PaymentMethodBound is the narrow marker accessor. An unknown or unstamped workspace returns false rather than leaking whether a tenant exists.
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) PurgeAuditEvents ¶
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) PurgeSSHSessions ¶
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 (*PGStore) PushDeliveryStats ¶
func (s *PGStore) PushDeliveryStats(ctx context.Context) (PushQueueStats, error)
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 ¶
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) (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.
It is CAS-guarded against a session a concurrent Cancel already took terminal (w2/m64): sandbox provisioning runs in the background after the create/steer verb has returned, so a Cancel can land while a sandbox is still coming up. The `phase NOT IN ('canceling','canceled')` guard makes the record a no-op in that race — the update matches no row and returns ErrNotFound — so the caller tears the just-created sandbox back down instead of resurrecting the session or orphaning the sandbox.
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) 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) RecordPushReceipt ¶
func (*PGStore) RecordPushSendFailure ¶
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) ReleaseGitWebhookDelivery ¶
ReleaseGitWebhookDelivery frees a claim whose delivery failed before completing (the webhook answered 5xx and the git host will redeliver): the retry must not be swallowed by a claim whose work never happened. A delivery that completed — even with per-app failures, which the webhook deliberately 200-swallows — keeps its claim: that IS the processed state.
func (*PGStore) ReleasePushDelivery ¶
ReleasePushDelivery leaves a failed provider attempt retryable without persisting provider text. expectedClaim protects a newer lease.
func (*PGStore) RemoveDomain ¶
RemoveDomain deletes a domain row for apps.IntentStore — idempotent (not-found silently ignored).
func (*PGStore) RemoveMember ¶
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 (*PGStore) RenameProject ¶
func (*PGStore) RenameTenant ¶
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 ¶
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) 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 (*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 (*PGStore) RevokeWebPushSubscription ¶
func (*PGStore) RunGroupingTx ¶
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 ¶
SSHKeyByFingerprint is the gateway-only lookup that maps a presented public key to its owning subject. It returns public metadata only.
func (*PGStore) SandboxKeyForWorkspace ¶
SandboxKeyForWorkspace returns the workspace's OpenSandbox tenant key, minting one on first use. Idempotent and race-safe: the UNIQUE(workspace_id) constraint collapses concurrent first-mints to a single key — ON CONFLICT returns the already-stored key rather than a second one. This is the KeyProvider the sandbox feature calls to stamp each request's OPEN-SANDBOX-API-KEY.
func (*PGStore) SandboxKeyLookup ¶
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) 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) 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) 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 ¶
SetAppDisplayName mirrors spec.displayName onto the row. Empty means never renamed, and readers fall back to apps.name.
func (*PGStore) SetAppEnvironment ¶
SetAppEnvironment atomically assigns an App row to an Environment and its owning Project. Empty ids clear both memberships (Blueprint ungrouped:).
func (*PGStore) SetAppIdleTTL ¶
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 ¶
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) SetAppReplicas ¶
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 (*PGStore) SetAppTier ¶
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) SetDeployPreDeployStatus ¶
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) 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) 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).
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) ([]string, 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 on departing rows in the same transaction (w4/m32) — a service leaving its project must not keep a stale apps.environment_id (and the App CR's frozen spec.environmentIPAllowList that implies): ListEnvironmentServices already filters on project_id too, so the row silently drops out of every future environment fan-out while its k8s-projected rules stay stuck. Returns the departing names that carried a non-null environment_id — the store layer's cue for the service layer's k8s-side clear, since a raw SQL UPDATE can't itself patch a CR. 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) 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 (*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) TenantForIdentity ¶
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 ¶
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) 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) TouchRegistryCredential ¶
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 (*PGStore) UnbindClient ¶
UnbindClient removes an API key's tenant_members row across every tenant it might be bound to (idempotent — a key that was never bound is not 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.
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) UpdateJobStatus ¶
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 ¶
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 ¶
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 ¶
UpsertBlueprint creates a blueprint or updates its fields 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.
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) UpsertGitConnection ¶
func (s *PGStore) UpsertGitConnection(ctx context.Context, c GitConnection) (GitConnection, error)
UpsertGitConnection records a connection, keyed by installation (ADR075). A re-connect of the same installation refreshes its workspace binding and account login; a new installation adds a row to the workspace's set. The caller (internal/github) enforces the one-workspace-per-installation and per-workspace-count invariants before this write. SECURITY (finding-4): the ON CONFLICT update is conditional on workspace_id matching so concurrent claims by two workspaces cannot silently transfer the installation. A cross-workspace conflict returns ErrConflict instead of overwriting the row.
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 ¶
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) 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 ¶
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 ¶
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 Project ¶
type Project struct {
ID string `json:"id"`
TenantID string `json:"tenantId"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
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 PushSweepResult ¶
type QuotaCaps ¶
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 ¶
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 {
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
// 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 string)
Rejection counts one refused conclusion. reason comes from the closed rejectReason* vocabulary — never anything tenant-derived, so label cardinality stays bounded regardless of fleet size.
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 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 ¶
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 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
}
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
// LegacyTarget is the old workspace-unique service:<public-name> audit key.
// It is matched only inside ownerWorkspace, never workspace:default; current
// writes use the namespace-unique CR-name target passed to ListServiceEvents.
LegacyTarget string
// AutoDeploy pushes down the auto-deploy boolean discrimination into SQL when
// the Verbs set includes apps.SetAutoDeploy. 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
// 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 only.
Verb string // e.g. "apps.Suspend"
Caller string // core.Identity.Subject
// 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
// 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
}
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; 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 and a status enum, 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. The reconciler's write-back
// closes it.
CreateDeploy(ctx context.Context, appID, trigger, image string, generation int64, commit CommitInfo) (Deploy, 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.
CreateRollbackDeploy(ctx context.Context, appID, image, rollbackOf string, generation int64, commit CommitInfo) (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 executing phase 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. A stale/repeated/invalid
// transition returns false without changing data.
TransitionDeploy(ctx context.Context, id, status, resolvedImage, failureReason, failureCode string) (bool, 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)
// 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)
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"`
}
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 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 ¶
UsageCoverage is the store's current-month evidence aggregate.
type UsageExportAttempt ¶
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 UsageSourceObservation ¶
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 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
// Latest* is populated only by ListWebhookEndpoints. It is the newest
// completed immutable attempt for this endpoint and the state of that
// attempt's logical notification, fetched in the list query rather than by
// one history query per endpoint.
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 ¶
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.
Source Files
¶
- advisory.go
- agentsessions.go
- api.go
- audit.go
- billing.go
- billing_lifecycle.go
- billing_operations.go
- blueprints.go
- cli_refresh.go
- deploy_lifecycle.go
- device_subscriptions.go
- environments.go
- event_facts.go
- events.go
- feedtail.go
- github.go
- gitwebhook.go
- grouping_tx.go
- jobs.go
- members.go
- migrate.go
- namespaces.go
- notifications.go
- oauth_revocations.go
- plans.go
- projects.go
- push_deliveries.go
- reconciler.go
- reconciler_metrics.go
- registrycredentials.go
- role_reconciliations.go
- sandbox_meter.go
- sandboxtenants.go
- sshkeys.go
- sshsessions.go
- store.go
- usage.go
- webhooks.go
- webpush_subscriptions.go
- workspaces.go