dbgen

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AcceptMFAStepParams added in v0.3.0

type AcceptMFAStepParams struct {
	Step   *int64
	UserID uuid.UUID
}

type AnalyticsSalt

type AnalyticsSalt struct {
	ValidOn   time.Time
	Salt      []byte
	CreatedAt time.Time
	PurgeAt   time.Time
}

type ApiKey

type ApiKey struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	Name           string
	Prefix         string
	KeyHash        []byte
	Scopes         []string
	LastUsedAt     *time.Time
	ExpiresAt      *time.Time
	RevokedAt      *time.Time
	CreatedAt      time.Time
	RotatedAt      *time.Time
	GraceExpiresAt *time.Time
	SuccessorID    *uuid.UUID
}

type ApiKeyOrgRevocation added in v0.3.0

type ApiKeyOrgRevocation struct {
	ApiKeyID       uuid.UUID
	OrganizationID uuid.UUID
	RevokedAt      time.Time
	RevokedBy      *uuid.UUID
}

type ArchiveLinkByAutomationParams added in v0.2.0

type ArchiveLinkByAutomationParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type ArchiveLinkByAutomationRow added in v0.2.0

type ArchiveLinkByAutomationRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	DomainID    uuid.UUID
	Alias       string
	PrimaryUrl  string
	Title       string
	Status      string
	ExpiresAt   *time.Time
	ArchivedAt  *time.Time
}

type ArchiveLinkParams

type ArchiveLinkParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type AttachTagParams

type AttachTagParams struct {
	LinkID      uuid.UUID
	TagID       uuid.UUID
	WorkspaceID uuid.UUID
}

type AuditLog

type AuditLog struct {
	ID             uuid.UUID
	OccurredAt     time.Time
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	ActorUserID    *uuid.UUID
	ActorLabel     string
	ActorApiKeyID  *uuid.UUID
	Action         string
	TargetType     *string
	TargetID       *uuid.UUID
	Metadata       []byte
	IpPrefix       *string
}

type AutomationRule

type AutomationRule struct {
	ID                 uuid.UUID
	WorkspaceID        uuid.UUID
	Name               string
	Trigger            string
	TriggerConfig      []byte
	Actions            []byte
	Enabled            bool
	LastFiredAt        *time.Time
	CreatedAt          time.Time
	UpdatedAt          time.Time
	LastCheckedAt      *time.Time
	LastFiredSubjectID *uuid.UUID
}

type BlockedDestination added in v0.2.0

type BlockedDestination struct {
	Host      string
	Source    string
	Reason    string
	CreatedAt time.Time
	CreatedBy *uuid.UUID
}

type Campaign

type Campaign struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
	Slug        string
	Description string
	Settings    []byte
	StartsAt    *time.Time
	EndsAt      *time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DeletedAt   *time.Time
}

type CarryAPIKeyReachRevocationsParams added in v0.3.0

type CarryAPIKeyReachRevocationsParams struct {
	SuccessorID   uuid.UUID
	PredecessorID uuid.UUID
}

type ClaimAutomationRuleParams added in v0.2.0

type ClaimAutomationRuleParams struct {
	Watermark        *time.Time
	WatermarkSubject *uuid.UUID
	ID               uuid.UUID
	Expected         *time.Time
	ExpectedSubject  *uuid.UUID
}

type ClaimDueMailParams added in v0.2.0

type ClaimDueMailParams struct {
	LeaseSeconds int32
	BatchSize    int32
}

type ClaimDueMailRow added in v0.2.0

type ClaimDueMailRow struct {
	ID        uuid.UUID
	Recipient string
	Subject   string
	Body      string
	Kind      string
	Attempts  int32
}

type ClaimDueWebhookDeliveriesParams added in v0.2.0

type ClaimDueWebhookDeliveriesParams struct {
	LeaseSeconds int32
	BatchSize    int32
}

type ClaimDueWebhookDeliveriesRow added in v0.2.0

type ClaimDueWebhookDeliveriesRow struct {
	ID        uuid.UUID
	WebhookID uuid.UUID
	Event     string
	Payload   []byte
	Attempts  int32
	Url       string
	Secret    []byte
}

type ClaimUpdateCheckParams added in v0.3.0

type ClaimUpdateCheckParams struct {
	At       time.Time
	NotSince time.Time
}

type ClearDefaultQRCodeParams added in v0.3.0

type ClearDefaultQRCodeParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type ClearQRCodeLogoParams added in v0.3.0

type ClearQRCodeLogoParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Style       []byte
}

type ClickEvent

type ClickEvent struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	OccurredAt    time.Time
	VisitorHash   []byte
	IsFirstVisit  bool
	Country       *string
	Region        *string
	City          *string
	Device        *string
	Browser       *string
	Os            *string
	Language      *string
	ReferrerHost  *string
	IsBot         bool
	LatencyUs     *int32
	DestinationID *uuid.UUID
}

type ConsumeClickBudgetParams added in v0.2.0

type ConsumeClickBudgetParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	ClickLimit  int64
}

type ConsumeClickBudgetRow added in v0.2.0

type ConsumeClickBudgetRow struct {
	Consumed  int64
	Exhausted bool
}

type CountDestinationWebhooksParams added in v0.2.0

type CountDestinationWebhooksParams struct {
	WorkspaceID uuid.UUID
	Events      []string
}

type CountLinksParams

type CountLinksParams struct {
	WorkspaceID  uuid.UUID
	Status       *string
	Search       *string
	TagIds       []uuid.UUID
	Unfiled      bool
	FolderID     *uuid.UUID
	Uncampaigned bool
	CampaignID   *uuid.UUID
	DomainID     *uuid.UUID
}

type CountMembershipsForEmailParams added in v0.2.0

type CountMembershipsForEmailParams struct {
	OrganizationID uuid.UUID
	Email          string
}

type CountMembershipsForUserParams added in v0.2.0

type CountMembershipsForUserParams struct {
	OrganizationID uuid.UUID
	UserID         uuid.UUID
}

type CountNotificationsAboutVersionParams added in v0.3.0

type CountNotificationsAboutVersionParams struct {
	UserID  uuid.UUID
	Kind    string
	Version string
}

type CountQRCodesParams added in v0.3.0

type CountQRCodesParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type CountRecentNotificationsOfKindParams added in v0.2.0

type CountRecentNotificationsOfKindParams struct {
	UserID uuid.UUID
	Kind   string
	Since  time.Time
}

type CountUnreadNotificationsParams added in v0.2.0

type CountUnreadNotificationsParams struct {
	UserID      uuid.UUID
	WorkspaceID *uuid.UUID
}

type CreateAPIKeyParams

type CreateAPIKeyParams struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	Name           string
	Prefix         string
	KeyHash        []byte
	Scopes         []string
	ExpiresAt      *time.Time
}

type CreateAutomationRuleParams added in v0.2.0

type CreateAutomationRuleParams struct {
	ID            uuid.UUID
	WorkspaceID   uuid.UUID
	Name          string
	Trigger       string
	TriggerConfig []byte
	Actions       []byte
	Enabled       bool
	LastFiredAt   *time.Time
}

type CreateCampaignParams added in v0.2.0

type CreateCampaignParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
	Slug        string
	Description string
	StartsAt    *time.Time
	EndsAt      *time.Time
}

type CreateDestinationParams

type CreateDestinationParams struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	UrlHost     string
}

type CreateDomainParams added in v0.2.0

type CreateDomainParams struct {
	ID                uuid.UUID
	OrganizationID    *uuid.UUID
	WorkspaceID       *uuid.UUID
	Hostname          string
	VerificationToken *string
}

type CreateFolderParams added in v0.2.0

type CreateFolderParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	ParentID    *uuid.UUID
	Name        string
}

type CreateInvitationParams added in v0.2.0

type CreateInvitationParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
	Email          string
	RoleID         uuid.UUID
	TokenHash      []byte
	InvitedBy      *uuid.UUID
	ExpiresAt      time.Time
}

type CreateLinkParams

type CreateLinkParams struct {
	ID               uuid.UUID
	WorkspaceID      uuid.UUID
	DomainID         uuid.UUID
	Alias            string
	PrimaryUrl       string
	Title            string
	Description      string
	Status           string
	ExpiresAt        *time.Time
	CreatedBy        *uuid.UUID
	ForwardQuery     bool
	ForwardPath      bool
	PasswordHash     *string
	MaxClicks        *int64
	OneTime          bool
	RequireSignature bool
	FolderID         *uuid.UUID
	CampaignID       *uuid.UUID
}

type CreateMFAPendingLoginParams added in v0.3.0

type CreateMFAPendingLoginParams struct {
	ID        uuid.UUID
	UserID    uuid.UUID
	TokenHash []byte
	IpPrefix  *string
	UserAgent *string
	ExpiresAt time.Time
}

type CreateMembershipParams

type CreateMembershipParams struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID uuid.UUID
	RoleID         uuid.UUID
	WorkspaceID    *uuid.UUID
}

type CreateOrganizationParams

type CreateOrganizationParams struct {
	ID         uuid.UUID
	Name       string
	Slug       string
	IsPersonal bool
}

type CreatePasswordResetParams added in v0.3.0

type CreatePasswordResetParams struct {
	ID        uuid.UUID
	UserID    uuid.UUID
	TokenHash []byte
	ExpiresAt time.Time
}

type CreatePendingRegistrationParams added in v0.2.0

type CreatePendingRegistrationParams struct {
	ID           uuid.UUID
	Email        string
	Name         string
	PasswordHash string
	TokenHash    []byte
	ExpiresAt    time.Time
}

type CreateRoutingRuleParams added in v0.2.0

type CreateRoutingRuleParams struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Priority      int32
	Conditions    []byte
	Enabled       bool
}

type CreateRuleDestinationParams added in v0.2.0

type CreateRuleDestinationParams struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	UrlHost     string
	Position    int32
}

type CreateSaltParams

type CreateSaltParams struct {
	ValidOn time.Time
	Salt    []byte
	PurgeAt time.Time
}

type CreateSessionParams

type CreateSessionParams struct {
	ID          uuid.UUID
	UserID      uuid.UUID
	TokenHash   []byte
	IpPrefix    *string
	UserAgent   *string
	ExpiresAt   time.Time
	WorkspaceID *uuid.UUID
}

type CreateTagParams

type CreateTagParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
	Color       string
}

type CreateUserParams

type CreateUserParams struct {
	ID              uuid.UUID
	Email           string
	Name            string
	PasswordHash    *string
	Status          string
	EmailVerifiedAt *time.Time
}

type CreateVariantDestinationParams added in v0.2.0

type CreateVariantDestinationParams struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	UrlHost     string
	Position    int32
	Weight      int32
}

type CreateVariantRuleParams added in v0.2.0

type CreateVariantRuleParams struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Enabled       bool
	Kind          string
}

type CreateWebhookParams added in v0.2.0

type CreateWebhookParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	Secret      []byte
	Events      []string
	Description string
	Enabled     bool
}

type CreateWebhookRow added in v0.2.0

type CreateWebhookRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	Events      []string
	Description string
	Enabled     bool
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type CreateWorkspaceParams

type CreateWorkspaceParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
	Name           string
	Slug           string
}

type DBTX

type DBTX interface {
	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
	QueryRow(context.Context, string, ...interface{}) pgx.Row
}

type DecideDestinationDisputeParams added in v0.2.0

type DecideDestinationDisputeParams struct {
	Status         string
	DecidedBy      *uuid.UUID
	DecidedByLabel string
	ID             uuid.UUID
}

type DeleteAccountDependentsRow added in v0.3.0

type DeleteAccountDependentsRow struct {
	Memberships      int64
	Sessions         int64
	ApiKeys          int64
	Notifications    int64
	PasswordResets   int64
	InstanceGrants   int64
	MfaRecoveryCodes int64
	MfaPendingLogins int64
}

type DeleteAutomationRuleParams added in v0.2.0

type DeleteAutomationRuleParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteCampaignParams added in v0.2.0

type DeleteCampaignParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteFolderParams added in v0.2.0

type DeleteFolderParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteMembershipParams added in v0.2.0

type DeleteMembershipParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
}

type DeleteQRCodeByIDParams added in v0.3.0

type DeleteQRCodeByIDParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteRoutingRuleParams added in v0.2.0

type DeleteRoutingRuleParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteRuleDestinationParams added in v0.2.0

type DeleteRuleDestinationParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteTagParams

type DeleteTagParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteVariantRuleParams added in v0.2.0

type DeleteVariantRuleParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteWebhookParams added in v0.2.0

type DeleteWebhookParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type DeleteWorkspaceParams added in v0.2.0

type DeleteWorkspaceParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
}

type Destination

type Destination struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	UrlHost     string
	Label       string
	Weight      int32
	Position    int32
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DeletedAt   *time.Time
}

type DestinationDispute added in v0.2.0

type DestinationDispute struct {
	ID             uuid.UUID
	Host           string
	UrlDefanged    string
	ReasonCode     string
	Status         string
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	CreatedBy      *uuid.UUID
	CreatedByLabel string
	CreatedAt      time.Time
	DecidedBy      *uuid.UUID
	DecidedByLabel string
	DecidedAt      *time.Time
	BlockedHost    string
}

type Domain

type Domain struct {
	ID                       uuid.UUID
	OrganizationID           *uuid.UUID
	Hostname                 string
	IsDefault                bool
	VerifiedAt               *time.Time
	SslStatus                string
	CreatedAt                time.Time
	UpdatedAt                time.Time
	DeletedAt                *time.Time
	RootRedirectUrl          *string
	BlockBots                bool
	BlockBotsEnforced        bool
	WorkspaceID              *uuid.UUID
	VerificationToken        *string
	VerificationCheckedAt    *time.Time
	VerificationFailingSince *time.Time
	VerificationError        *string
}

type EnableUserMFAParams added in v0.3.0

type EnableUserMFAParams struct {
	Secret    *string
	FirstStep *int64
	UserID    uuid.UUID
}

type EnqueueMailParams added in v0.2.0

type EnqueueMailParams struct {
	ID        uuid.UUID
	Recipient string
	Subject   string
	Body      string
	Kind      string
}

type EnqueueWebhookDeliveriesParams added in v0.2.0

type EnqueueWebhookDeliveriesParams struct {
	Event       string
	Payload     []byte
	WorkspaceID uuid.UUID
}

type EnsureWorkspaceSigningSecretParams added in v0.2.0

type EnsureWorkspaceSigningSecretParams struct {
	Candidate []byte
	ID        uuid.UUID
}

type EraseDeletedAccountsParams added in v0.3.0

type EraseDeletedAccountsParams struct {
	Batch     int32
	Tombstone string
}

type Folder

type Folder struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	ParentID    *uuid.UUID
	Name        string
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DeletedAt   *time.Time
}

type GetAPIKeyByPrefixRow

type GetAPIKeyByPrefixRow struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	KeyHash        []byte
	Scopes         []string
	ExpiresAt      *time.Time
	RevokedAt      *time.Time
	GraceExpiresAt *time.Time
	Email          string
	UserName       string
	Status         string
	OwnerIsMember  bool
}

type GetAPIKeyForRotationRow added in v0.2.0

type GetAPIKeyForRotationRow struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	Name           string
	Prefix         string
	Scopes         []string
	ExpiresAt      *time.Time
	RevokedAt      *time.Time
	RotatedAt      *time.Time
	GraceExpiresAt *time.Time
	SuccessorID    *uuid.UUID
	CreatedAt      time.Time
	OwnerIsMember  bool
	OwnerStatus    interface{}
}

type GetAPIKeyReachRow added in v0.3.0

type GetAPIKeyReachRow struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID *uuid.UUID
	Prefix         string
	RevokedAt      *time.Time
}

type GetAutomationRuleParams added in v0.2.0

type GetAutomationRuleParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetBlockedDestinationRow added in v0.2.0

type GetBlockedDestinationRow struct {
	Host   string
	Source string
	Reason string
}

type GetBuiltinRoleBySlugRow added in v0.2.0

type GetBuiltinRoleBySlugRow struct {
	ID   uuid.UUID
	Slug string
	Name string
	Rank int32
}

type GetCampaignParams added in v0.2.0

type GetCampaignParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetClickBudgetRow added in v0.2.0

type GetClickBudgetRow struct {
	Consumed    int64
	ExhaustedAt *time.Time
}

type GetDefaultDomainSettingsRow

type GetDefaultDomainSettingsRow struct {
	ID                uuid.UUID
	Hostname          string
	RootRedirectUrl   *string
	BlockBots         bool
	BlockBotsEnforced bool
}

type GetDefaultQRCodeParams added in v0.3.0

type GetDefaultQRCodeParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type GetDefaultQRCodeRow added in v0.3.0

type GetDefaultQRCodeRow struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Style       []byte
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Label       string
	Slug        string
	IsDefault   bool
}

type GetDestinationDisputeRow added in v0.2.0

type GetDestinationDisputeRow struct {
	ID             uuid.UUID
	Host           string
	UrlDefanged    string
	ReasonCode     string
	Status         string
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	CreatedBy      *uuid.UUID
	CreatedByLabel string
	CreatedAt      time.Time
	DecidedBy      *uuid.UUID
	DecidedByLabel string
	DecidedAt      *time.Time
	BlockedHost    string
	EntrySource    *string
}

type GetDomainBotSettingsRow added in v0.2.0

type GetDomainBotSettingsRow struct {
	ID                uuid.UUID
	Hostname          string
	BlockBots         bool
	BlockBotsEnforced bool
}

type GetFolderParams added in v0.2.0

type GetFolderParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetInvitationByTokenHashRow added in v0.2.0

type GetInvitationByTokenHashRow struct {
	ID               uuid.UUID
	OrganizationID   uuid.UUID
	Email            string
	EmailLower       *string
	RoleID           uuid.UUID
	InvitedBy        *uuid.UUID
	CreatedAt        time.Time
	ExpiresAt        time.Time
	RevokedAt        *time.Time
	RedeemedAt       *time.Time
	RoleSlug         string
	OrganizationName string
}

type GetJobStalenessRow added in v0.2.0

type GetJobStalenessRow struct {
	Job          string
	StaleSeconds float64
}

type GetLinkByAliasParams

type GetLinkByAliasParams struct {
	DomainID uuid.UUID
	Alias    string
}

type GetLinkDimensionsParams

type GetLinkDimensionsParams struct {
	LinkID    uuid.UUID
	Dimension string
	FromDay   time.Time
	ToDay     time.Time
	RowLimit  int32
}

type GetLinkDimensionsRow

type GetLinkDimensionsRow struct {
	Value          string
	Clicks         int64
	UniqueVisitors int64
}

type GetLinkParams

type GetLinkParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetLinkQRDimensionsParams added in v0.3.0

type GetLinkQRDimensionsParams struct {
	LinkID   uuid.UUID
	FromDay  time.Time
	ToDay    time.Time
	RowLimit int32
}

type GetLinkQRDimensionsRow added in v0.3.0

type GetLinkQRDimensionsRow struct {
	Value          string
	Clicks         int64
	UniqueVisitors int64
}

type GetLinkStatsParams

type GetLinkStatsParams struct {
	LinkID  uuid.UUID
	FromDay time.Time
	ToDay   time.Time
}

type GetLinkStatsRow

type GetLinkStatsRow struct {
	Day            time.Time
	Clicks         int64
	UniqueVisitors int64
	BotClicks      int64
}

type GetLinkTagsRow

type GetLinkTagsRow struct {
	ID    uuid.UUID
	Name  string
	Color string
}

type GetMembershipParams added in v0.2.0

type GetMembershipParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
}

type GetMembershipRow added in v0.2.0

type GetMembershipRow struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID uuid.UUID
	WorkspaceID    *uuid.UUID
	Email          string
	Name           string
	RoleSlug       string
	RoleRank       int32
}

type GetNotificationParams added in v0.3.0

type GetNotificationParams struct {
	ID     uuid.UUID
	UserID uuid.UUID
}

type GetOrganizationMemberParams added in v0.2.0

type GetOrganizationMemberParams struct {
	OrganizationID uuid.UUID
	UserID         uuid.UUID
}

type GetOrganizationMemberRow added in v0.2.0

type GetOrganizationMemberRow struct {
	ID       uuid.UUID
	UserID   uuid.UUID
	Email    string
	Name     string
	RoleSlug string
	RoleRank int32
}

type GetPasswordResetByTokenHashRow added in v0.3.0

type GetPasswordResetByTokenHashRow struct {
	ID           uuid.UUID
	UserID       uuid.UUID
	TokenHash    []byte
	CreatedAt    time.Time
	ExpiresAt    time.Time
	ConsumedAt   *time.Time
	Email        string
	Name         string
	Status       string
	PasswordHash *string
}

type GetQRCodeLogoParams added in v0.3.0

type GetQRCodeLogoParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Slug        string
}

type GetQRCodeParams added in v0.2.0

type GetQRCodeParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Slug        string
}

type GetQRCodeRow added in v0.3.0

type GetQRCodeRow struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Style       []byte
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Label       string
	Slug        string
	IsDefault   bool
}

type GetRecentClicksParams

type GetRecentClicksParams struct {
	LinkID   uuid.UUID
	RowLimit int32
}

type GetRecentClicksRow

type GetRecentClicksRow struct {
	OccurredAt   time.Time
	Device       *string
	Browser      *string
	Os           *string
	Country      *string
	ReferrerHost *string
	IsBot        bool
}

type GetRoutingRuleParams added in v0.2.0

type GetRoutingRuleParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetRoutingRuleRow added in v0.2.0

type GetRoutingRuleRow struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Priority      int32
	Conditions    []byte
	Enabled       bool
	CreatedAt     time.Time
	UpdatedAt     time.Time
	Url           string
}

type GetSessionByTokenHashRow

type GetSessionByTokenHashRow struct {
	ID           uuid.UUID
	UserID       uuid.UUID
	CreatedAt    time.Time
	LastSeenAt   time.Time
	ExpiresAt    time.Time
	Email        string
	Name         string
	Status       string
	PasswordHash *string
}

type GetTagByNameParams

type GetTagByNameParams struct {
	WorkspaceID uuid.UUID
	Lower       string
}

type GetUserMFARow added in v0.3.0

type GetUserMFARow struct {
	ID           uuid.UUID
	Email        string
	Name         string
	Status       string
	MfaSecret    *string
	MfaEnabledAt *time.Time
	MfaLastStep  *int64
}

type GetUserPermissionsParams

type GetUserPermissionsParams struct {
	UserID uuid.UUID
	ID     uuid.UUID
}

type GetUserRoleInWorkspaceParams

type GetUserRoleInWorkspaceParams struct {
	UserID uuid.UUID
	ID     uuid.UUID
}

type GetUserRoleInWorkspaceRow

type GetUserRoleInWorkspaceRow struct {
	Slug string
	Name string
	Rank int32
}

type GetVariantRuleParams added in v0.2.0

type GetVariantRuleParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetVariantRuleRow added in v0.2.0

type GetVariantRuleRow struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Kind          string
	Enabled       bool
	CreatedAt     time.Time
	UpdatedAt     time.Time
	Url           string
	Weight        int32
	Position      int32
}

type GetWebhookParams added in v0.2.0

type GetWebhookParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type GetWebhookRow added in v0.2.0

type GetWebhookRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	Events      []string
	Description string
	Enabled     bool
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type GetWorkspaceDefaultDomainParams added in v0.2.0

type GetWorkspaceDefaultDomainParams struct {
	WorkspaceID    *uuid.UUID
	OrganizationID *uuid.UUID
}

type GetWorkspaceDefaultDomainRow

type GetWorkspaceDefaultDomainRow struct {
	ID             uuid.UUID
	Hostname       string
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
}

type GetWorkspaceInOrganizationParams added in v0.2.0

type GetWorkspaceInOrganizationParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
}

type GetWorkspaceStatsParams

type GetWorkspaceStatsParams struct {
	WorkspaceID uuid.UUID
	FromDay     time.Time
	ToDay       time.Time
}

type GetWorkspaceStatsRow

type GetWorkspaceStatsRow struct {
	Day            time.Time
	Clicks         int64
	UniqueVisitors int64
	BotClicks      int64
	ActiveLinks    int64
}

type GetWorkspaceTotalsParams

type GetWorkspaceTotalsParams struct {
	WorkspaceID uuid.UUID
	FromDay     time.Time
	ToDay       time.Time
}

type GetWorkspaceTotalsRow

type GetWorkspaceTotalsRow struct {
	Clicks         int64
	UniqueVisitors int64
	BotClicks      int64
}

type GrantInstancePermissionParams added in v0.2.0

type GrantInstancePermissionParams struct {
	UserID     uuid.UUID
	GrantedBy  *uuid.UUID
	Permission string
}

type InsertAuditLogParams added in v0.2.0

type InsertAuditLogParams struct {
	ID             uuid.UUID
	OccurredAt     time.Time
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	ActorUserID    *uuid.UUID
	ActorLabel     string
	ActorApiKeyID  *uuid.UUID
	Action         string
	TargetType     *string
	TargetID       *uuid.UUID
	Metadata       []byte
	IpPrefix       *string
}

type InsertDestinationDisputeParams added in v0.2.0

type InsertDestinationDisputeParams struct {
	ID             uuid.UUID
	Host           string
	BlockedHost    string
	UrlDefanged    string
	ReasonCode     string
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	CreatedBy      *uuid.UUID
	CreatedByLabel string
}

type InsertMFARecoveryCodeParams added in v0.3.0

type InsertMFARecoveryCodeParams struct {
	ID       uuid.UUID
	UserID   uuid.UUID
	CodeHash []byte
}

type InsertNotificationParams added in v0.2.0

type InsertNotificationParams struct {
	ID          uuid.UUID
	UserID      uuid.UUID
	WorkspaceID *uuid.UUID
	Kind        string
	Title       string
	Body        string
	Data        []byte
}

type InstanceGrant added in v0.2.0

type InstanceGrant struct {
	UserID       uuid.UUID
	PermissionID uuid.UUID
	GrantedBy    *uuid.UUID
	GrantedAt    time.Time
}

type InstanceSetting added in v0.3.0

type InstanceSetting struct {
	ID                 bool
	UpdateCheckEnabled *bool
	UpdateCheckedAt    *time.Time
	UpdatedAt          time.Time
}

type Invitation added in v0.2.0

type Invitation struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
	Email          string
	EmailLower     *string
	RoleID         uuid.UUID
	TokenHash      []byte
	InvitedBy      *uuid.UUID
	CreatedAt      time.Time
	ExpiresAt      time.Time
	RevokedAt      *time.Time
	RedeemedAt     *time.Time
	RedeemedBy     *uuid.UUID
}

type IsAliasTakenParams

type IsAliasTakenParams struct {
	DomainID uuid.UUID
	Alias    string
}

type JobState

type JobState struct {
	Job           string
	LastRunAt     *time.Time
	Watermark     *time.Time
	LastError     *string
	UpdatedAt     time.Time
	LastSuccessAt *time.Time
}
type Link struct {
	ID                   uuid.UUID
	WorkspaceID          uuid.UUID
	DomainID             uuid.UUID
	FolderID             *uuid.UUID
	Alias                string
	PrimaryUrl           string
	PrimaryDestinationID *uuid.UUID
	Title                string
	Description          string
	Status               string
	ExpiresAt            *time.Time
	PasswordHash         *string
	MaxClicks            *int64
	OneTime              bool
	ForwardQuery         bool
	ClickCount           int64
	LastClickAt          *time.Time
	CreatedBy            *uuid.UUID
	CreatedAt            time.Time
	UpdatedAt            time.Time
	ArchivedAt           *time.Time
	DeletedAt            *time.Time
	PurgeAfter           *time.Time
	SearchVector         interface{}
	CampaignID           *uuid.UUID
	BotBlocking          string
	ForwardPath          bool
	RequireSignature     bool
}

type LinkClickBudget added in v0.2.0

type LinkClickBudget struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Consumed    int64
	ExhaustedAt *time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Rotation    int64
}

type LinkClickDaily

type LinkClickDaily struct {
	LinkID         uuid.UUID
	WorkspaceID    uuid.UUID
	Day            time.Time
	Clicks         int64
	UniqueVisitors int64
	BotClicks      int64
	FinalizedAt    *time.Time
}

type LinkDimensionDaily

type LinkDimensionDaily struct {
	LinkID         uuid.UUID
	WorkspaceID    uuid.UUID
	Day            time.Time
	Dimension      string
	Value          string
	Clicks         int64
	UniqueVisitors int64
}

type LinkTag

type LinkTag struct {
	LinkID      uuid.UUID
	TagID       uuid.UUID
	WorkspaceID uuid.UUID
}

type ListAPIKeyOrgRevocationsRow added in v0.3.0

type ListAPIKeyOrgRevocationsRow struct {
	ApiKeyID         uuid.UUID
	OrganizationID   uuid.UUID
	OrganizationName string
	RevokedAt        time.Time
}

type ListAPIKeysForUserRow

type ListAPIKeysForUserRow struct {
	ID             uuid.UUID
	Name           string
	Prefix         string
	Scopes         []string
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	LastUsedAt     *time.Time
	ExpiresAt      *time.Time
	RevokedAt      *time.Time
	RotatedAt      *time.Time
	GraceExpiresAt *time.Time
	SuccessorID    *uuid.UUID
	CreatedAt      time.Time
}

type ListAuditLogsParams added in v0.2.0

type ListAuditLogsParams struct {
	OrganizationID *uuid.UUID
	OrgWide        bool
	WorkspaceIds   []uuid.UUID
	CursorOccurred *time.Time
	CursorID       *uuid.UUID
	PageLimit      int32
}

type ListBuiltinRolesRow added in v0.2.0

type ListBuiltinRolesRow struct {
	ID          uuid.UUID
	Slug        string
	Name        string
	Description string
	Rank        int32
}

type ListCampaignsRow added in v0.2.0

type ListCampaignsRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
	Slug        string
	Description string
	Settings    []byte
	StartsAt    *time.Time
	EndsAt      *time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DeletedAt   *time.Time
	LinkCount   int64
}

type ListDestinationDisputesParams added in v0.2.0

type ListDestinationDisputesParams struct {
	OpenOnly      bool
	CursorCreated *time.Time
	CursorID      *uuid.UUID
	PageLimit     int32
}

type ListDestinationDisputesRow added in v0.2.0

type ListDestinationDisputesRow struct {
	ID             uuid.UUID
	Host           string
	UrlDefanged    string
	ReasonCode     string
	Status         string
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
	CreatedBy      *uuid.UUID
	CreatedByLabel string
	CreatedAt      time.Time
	DecidedBy      *uuid.UUID
	DecidedByLabel string
	DecidedAt      *time.Time
	BlockedHost    string
	EntrySource    *string
}

type ListDomainsParams added in v0.2.0

type ListDomainsParams struct {
	OrganizationID *uuid.UUID
	WorkspaceID    *uuid.UUID
}

type ListDomainsRow added in v0.2.0

type ListDomainsRow struct {
	ID                       uuid.UUID
	OrganizationID           *uuid.UUID
	Hostname                 string
	IsDefault                bool
	VerifiedAt               *time.Time
	SslStatus                string
	CreatedAt                time.Time
	UpdatedAt                time.Time
	DeletedAt                *time.Time
	RootRedirectUrl          *string
	BlockBots                bool
	BlockBotsEnforced        bool
	WorkspaceID              *uuid.UUID
	VerificationToken        *string
	VerificationCheckedAt    *time.Time
	VerificationFailingSince *time.Time
	VerificationError        *string
	LinkCount                int64
}

type ListDueAutomationRulesRow added in v0.2.0

type ListDueAutomationRulesRow struct {
	ID                 uuid.UUID
	WorkspaceID        uuid.UUID
	OrganizationID     uuid.UUID
	Name               string
	Trigger            string
	TriggerConfig      []byte
	Actions            []byte
	LastFiredAt        *time.Time
	LastFiredSubjectID *uuid.UUID
	CreatedAt          time.Time
}

type ListFoldersRow added in v0.2.0

type ListFoldersRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	ParentID    *uuid.UUID
	Name        string
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DeletedAt   *time.Time
	LinkCount   int64
}

type ListInstanceAuditLogsParams added in v0.2.0

type ListInstanceAuditLogsParams struct {
	CursorOccurred *time.Time
	CursorID       *uuid.UUID
	PageLimit      int32
}

type ListInstanceGrantHoldersRow added in v0.2.0

type ListInstanceGrantHoldersRow struct {
	ID        uuid.UUID
	Email     string
	Name      string
	GrantedAt time.Time
	GrantedBy *uuid.UUID
}

type ListInvitationsRow added in v0.2.0

type ListInvitationsRow struct {
	ID             uuid.UUID
	Email          string
	CreatedAt      time.Time
	ExpiresAt      time.Time
	RevokedAt      *time.Time
	RedeemedAt     *time.Time
	RoleSlug       string
	RoleRank       int32
	InvitedByLabel string
}

type ListLinkDestinationsParams added in v0.2.0

type ListLinkDestinationsParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type ListLinkDestinationsRow added in v0.2.0

type ListLinkDestinationsRow struct {
	ID        uuid.UUID
	Url       string
	Weight    int32
	Position  int32
	IsPrimary bool
}

type ListLinksParams

type ListLinksParams struct {
	WorkspaceID   uuid.UUID
	Status        *string
	Search        *string
	TagIds        []uuid.UUID
	Unfiled       bool
	FolderID      *uuid.UUID
	Uncampaigned  bool
	CampaignID    *uuid.UUID
	DomainID      *uuid.UUID
	CursorID      *uuid.UUID
	Sort          string
	CursorCreated *time.Time
	CursorClicks  *int64
	PageLimit     int32
}

type ListLinksRow

type ListLinksRow struct {
	ID                   uuid.UUID
	WorkspaceID          uuid.UUID
	DomainID             uuid.UUID
	FolderID             *uuid.UUID
	Alias                string
	PrimaryUrl           string
	PrimaryDestinationID *uuid.UUID
	Title                string
	Description          string
	Status               string
	ExpiresAt            *time.Time
	PasswordHash         *string
	MaxClicks            *int64
	OneTime              bool
	ForwardQuery         bool
	ClickCount           int64
	LastClickAt          *time.Time
	CreatedBy            *uuid.UUID
	CreatedAt            time.Time
	UpdatedAt            time.Time
	ArchivedAt           *time.Time
	DeletedAt            *time.Time
	PurgeAfter           *time.Time
	SearchVector         interface{}
	CampaignID           *uuid.UUID
	BotBlocking          string
	ForwardPath          bool
	RequireSignature     bool
	TagNames             []string
	TagIds               []string
}

type ListMembersRow added in v0.2.0

type ListMembersRow struct {
	ID            uuid.UUID
	UserID        uuid.UUID
	WorkspaceID   *uuid.UUID
	CreatedAt     time.Time
	Email         string
	Name          string
	Status        string
	RoleSlug      string
	RoleName      string
	RoleRank      int32
	WorkspaceName *string
}

type ListMembershipAuthorityParams added in v0.2.0

type ListMembershipAuthorityParams struct {
	Permission     string
	UserID         uuid.UUID
	OrganizationID uuid.UUID
}

type ListMembershipAuthorityRow added in v0.2.0

type ListMembershipAuthorityRow struct {
	ID               uuid.UUID
	WorkspaceID      *uuid.UUID
	RoleSlug         string
	RoleRank         int32
	GrantsPermission bool
}

type ListNotificationsParams added in v0.2.0

type ListNotificationsParams struct {
	UserID        uuid.UUID
	WorkspaceID   *uuid.UUID
	UnreadOnly    bool
	CursorCreated *time.Time
	CursorID      *uuid.UUID
	PageLimit     int32
}

type ListPendingDomainsForVerificationRow added in v0.2.0

type ListPendingDomainsForVerificationRow struct {
	ID       uuid.UUID
	Hostname string
}

type ListQRCodesParams added in v0.3.0

type ListQRCodesParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type ListQRCodesRow added in v0.3.0

type ListQRCodesRow struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Style       []byte
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Label       string
	Slug        string
	IsDefault   bool
}

type ListRoutingRulesParams added in v0.2.0

type ListRoutingRulesParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type ListRoutingRulesRow added in v0.2.0

type ListRoutingRulesRow struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Priority      int32
	Conditions    []byte
	Enabled       bool
	CreatedAt     time.Time
	UpdatedAt     time.Time
	Url           string
}

type ListServingDomainsForVerificationRow added in v0.2.0

type ListServingDomainsForVerificationRow struct {
	ID       uuid.UUID
	Hostname string
}

type ListTagsRow

type ListTagsRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
	Color       string
	CreatedAt   time.Time
	LinkCount   int64
}

type ListUnreadNotificationPreviewParams added in v0.2.0

type ListUnreadNotificationPreviewParams struct {
	UserID      uuid.UUID
	WorkspaceID *uuid.UUID
	PageLimit   int32
}

type ListUnreadNotificationPreviewRow added in v0.2.0

type ListUnreadNotificationPreviewRow struct {
	ID          uuid.UUID
	UserID      uuid.UUID
	WorkspaceID *uuid.UUID
	Kind        string
	Title       string
	Body        string
	Data        []byte
	ReadAt      *time.Time
	CreatedAt   time.Time
	UnreadTotal int64
}

type ListUserSessionsRow

type ListUserSessionsRow struct {
	ID         uuid.UUID
	IpPrefix   *string
	UserAgent  *string
	CreatedAt  time.Time
	LastSeenAt time.Time
	ExpiresAt  time.Time
}

type ListUsersRow

type ListUsersRow struct {
	ID          uuid.UUID
	Email       string
	Name        string
	Status      string
	LastLoginAt *time.Time
	CreatedAt   time.Time
}

type ListUsersWithRoleInOrgParams added in v0.2.0

type ListUsersWithRoleInOrgParams struct {
	OrganizationID uuid.UUID
	WorkspaceID    *uuid.UUID
	RoleSlug       string
}

type ListUsersWithRoleInOrgRow added in v0.2.0

type ListUsersWithRoleInOrgRow struct {
	ID    uuid.UUID
	Email string
	Name  string
}

type ListVariantRulesParams added in v0.2.0

type ListVariantRulesParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type ListVariantRulesRow added in v0.2.0

type ListVariantRulesRow struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Kind          string
	Enabled       bool
	CreatedAt     time.Time
	UpdatedAt     time.Time
	Url           string
	Weight        int32
	Position      int32
}

type ListVerifiedDomainsRow added in v0.2.0

type ListVerifiedDomainsRow struct {
	ID              uuid.UUID
	Hostname        string
	RootRedirectUrl *string
	SslStatus       string
}

type ListWebhookDeliveriesParams added in v0.2.0

type ListWebhookDeliveriesParams struct {
	WebhookID   uuid.UUID
	WorkspaceID uuid.UUID
	RowLimit    int32
}

type ListWebhookDeliveriesRow added in v0.2.0

type ListWebhookDeliveriesRow struct {
	ID            uuid.UUID
	Event         string
	Status        string
	Attempts      int32
	ResponseCode  *int32
	LastError     string
	NextAttemptAt *time.Time
	CreatedAt     time.Time
	CompletedAt   *time.Time
}

type ListWebhooksRow added in v0.2.0

type ListWebhooksRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	Events      []string
	Description string
	Enabled     bool
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type ListWorkspacesForUserRow added in v0.2.0

type ListWorkspacesForUserRow struct {
	ID               uuid.UUID
	Name             string
	Slug             string
	OrganizationID   uuid.UUID
	OrganizationName string
	IsPersonal       bool
	IsDefault        bool
}

type LockMFAPendingLoginRow added in v0.3.0

type LockMFAPendingLoginRow struct {
	ID           uuid.UUID
	UserID       uuid.UUID
	IpPrefix     *string
	UserAgent    *string
	CreatedAt    time.Time
	ExpiresAt    time.Time
	ConsumedAt   *time.Time
	Email        string
	Name         string
	Status       string
	MfaSecret    *string
	MfaEnabledAt *time.Time
	MfaLastStep  *int64
}

type LockOrganizationsSolelyOwnedByRow added in v0.3.0

type LockOrganizationsSolelyOwnedByRow struct {
	ID   uuid.UUID
	Name string
	Slug string
}

type LockUserMFARow added in v0.3.0

type LockUserMFARow struct {
	ID           uuid.UUID
	Email        string
	Name         string
	Status       string
	MfaSecret    *string
	MfaEnabledAt *time.Time
	MfaLastStep  *int64
}

type MailOutbox added in v0.2.0

type MailOutbox struct {
	ID            uuid.UUID
	Recipient     string
	Subject       string
	Body          string
	Kind          string
	Status        string
	Attempts      int32
	NextAttemptAt time.Time
	LastError     string
	CreatedAt     time.Time
	SentAt        *time.Time
}

type MarkAPIKeyRotatedParams added in v0.2.0

type MarkAPIKeyRotatedParams struct {
	ID             uuid.UUID
	GraceExpiresAt *time.Time
	SuccessorID    *uuid.UUID
}

type MarkAutomationRulesCheckedParams added in v0.2.0

type MarkAutomationRulesCheckedParams struct {
	CheckedAt *time.Time
	Ids       []uuid.UUID
}

type MarkDefaultQRCodeParams added in v0.3.0

type MarkDefaultQRCodeParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type MarkDomainVerificationFailedParams added in v0.2.0

type MarkDomainVerificationFailedParams struct {
	VerificationError string
	ID                uuid.UUID
	Hostname          string
	VerificationToken *string
}

type MarkDomainVerifiedParams added in v0.2.0

type MarkDomainVerifiedParams struct {
	ID                uuid.UUID
	Hostname          string
	VerificationToken *string
}

type MarkInvitationRedeemedParams added in v0.2.0

type MarkInvitationRedeemedParams struct {
	ID         uuid.UUID
	RedeemedBy *uuid.UUID
}

type MarkMailFailedParams added in v0.2.0

type MarkMailFailedParams struct {
	LastError string
	ID        uuid.UUID
}

type MarkMailRetryParams added in v0.2.0

type MarkMailRetryParams struct {
	BackoffSeconds int32
	LastError      string
	ID             uuid.UUID
}

type MarkNotificationReadParams added in v0.2.0

type MarkNotificationReadParams struct {
	ID     uuid.UUID
	UserID uuid.UUID
}

type MarkNotificationUnreadParams added in v0.3.0

type MarkNotificationUnreadParams struct {
	ID     uuid.UUID
	UserID uuid.UUID
}

type MarkWebhookAbandonedParams added in v0.2.0

type MarkWebhookAbandonedParams struct {
	ResponseCode *int32
	LastError    string
	ID           uuid.UUID
}

type MarkWebhookDeliveredParams added in v0.2.0

type MarkWebhookDeliveredParams struct {
	ResponseCode *int32
	ID           uuid.UUID
}

type MarkWebhookRetryParams added in v0.2.0

type MarkWebhookRetryParams struct {
	BackoffSeconds int32
	ResponseCode   *int32
	LastError      string
	ID             uuid.UUID
}

type MatchBlockedDestinationRow added in v0.2.0

type MatchBlockedDestinationRow struct {
	Host   string
	Source string
	Reason string
}

type MatchExhaustedBudgetsParams added in v0.2.0

type MatchExhaustedBudgetsParams struct {
	WorkspaceID  uuid.UUID
	After        *time.Time
	AfterSubject uuid.UUID
	Until        *time.Time
	RowLimit     int32
}

type MatchExhaustedBudgetsRow added in v0.2.0

type MatchExhaustedBudgetsRow struct {
	LinkID      uuid.UUID
	Alias       string
	ExhaustedAt *time.Time
}

type MatchExpiredLinksParams added in v0.2.0

type MatchExpiredLinksParams struct {
	WorkspaceID  uuid.UUID
	After        *time.Time
	AfterSubject uuid.UUID
	Until        *time.Time
	RowLimit     int32
}

type MatchExpiredLinksRow added in v0.2.0

type MatchExpiredLinksRow struct {
	ID        uuid.UUID
	Alias     string
	ExpiresAt *time.Time
}

type MatchWorkspaceAuditEventsParams added in v0.2.0

type MatchWorkspaceAuditEventsParams struct {
	WorkspaceID  *uuid.UUID
	Action       string
	After        time.Time
	AfterSubject uuid.UUID
	Until        time.Time
	RowLimit     int32
}

type MatchWorkspaceAuditEventsRow added in v0.2.0

type MatchWorkspaceAuditEventsRow struct {
	ID         uuid.UUID
	OccurredAt time.Time
	Metadata   []byte
}

type Membership

type Membership struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	OrganizationID uuid.UUID
	RoleID         uuid.UUID
	WorkspaceID    *uuid.UUID
	CreatedAt      time.Time
	UpdatedAt      time.Time
}

type MfaPendingLogin added in v0.3.0

type MfaPendingLogin struct {
	ID         uuid.UUID
	UserID     uuid.UUID
	TokenHash  []byte
	IpPrefix   *string
	UserAgent  *string
	CreatedAt  time.Time
	ExpiresAt  time.Time
	ConsumedAt *time.Time
}

type MfaRecoveryCode added in v0.3.0

type MfaRecoveryCode struct {
	ID        uuid.UUID
	UserID    uuid.UUID
	CodeHash  []byte
	CreatedAt time.Time
	UsedAt    *time.Time
}

type MoveFolderParams added in v0.2.0

type MoveFolderParams struct {
	ParentID    *uuid.UUID
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type NameQRCodeParams added in v0.3.0

type NameQRCodeParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Slug        string
}

type NextVariantRotationParams added in v0.2.0

type NextVariantRotationParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type Notification

type Notification struct {
	ID          uuid.UUID
	UserID      uuid.UUID
	WorkspaceID *uuid.UUID
	Kind        string
	Title       string
	Body        string
	Data        []byte
	ReadAt      *time.Time
	CreatedAt   time.Time
}

type OldestQRCodeParams added in v0.3.0

type OldestQRCodeParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	ID          uuid.UUID
}

type OldestQRCodeRow added in v0.3.0

type OldestQRCodeRow struct {
	ID   uuid.UUID
	Slug string
}

type Organization

type Organization struct {
	ID         uuid.UUID
	Name       string
	Slug       string
	DataRegion string
	IsPersonal bool
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DeletedAt  *time.Time
}

type PasswordReset added in v0.3.0

type PasswordReset struct {
	ID         uuid.UUID
	UserID     uuid.UUID
	TokenHash  []byte
	CreatedAt  time.Time
	ExpiresAt  time.Time
	ConsumedAt *time.Time
}

type PeekInvitationByTokenHashRow added in v0.2.0

type PeekInvitationByTokenHashRow struct {
	ID               uuid.UUID
	OrganizationID   uuid.UUID
	Email            string
	EmailLower       *string
	RoleID           uuid.UUID
	InvitedBy        *uuid.UUID
	CreatedAt        time.Time
	ExpiresAt        time.Time
	RevokedAt        *time.Time
	RedeemedAt       *time.Time
	RoleSlug         string
	OrganizationName string
}

type PeekVariantRotationParams added in v0.2.0

type PeekVariantRotationParams struct {
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
}

type PendingRegistration added in v0.2.0

type PendingRegistration struct {
	ID           uuid.UUID
	Email        string
	EmailLower   *string
	Name         string
	PasswordHash string
	TokenHash    []byte
	CreatedAt    time.Time
	ExpiresAt    time.Time
	ConsumedAt   *time.Time
}

type Permission

type Permission struct {
	ID          uuid.UUID
	Slug        string
	Description string
}

type PurgeExpiredLinksRow

type PurgeExpiredLinksRow struct {
	Alias    string
	Reserved bool
}

type PurgeFinishedPasswordResetsParams added in v0.3.0

type PurgeFinishedPasswordResetsParams struct {
	KeepDays int32
	Batch    int32
}

type QrCode

type QrCode struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Style       []byte
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Label       string
	Slug        string
	IsDefault   bool
}

type Querier

type Querier interface {
	//
	// Fails pending rows on an instance that has no relay to send them.
	//
	// The outbox has one guard against unbounded growth — PurgeFinishedMail — and it
	// takes only `status <> 'pending'`, which is correct while a mailer exists:
	// Drain claims every pending row and moves it to sent or, after five attempts,
	// failed, and the lease recovers rows a crash interrupted. So nothing stays
	// pending, and nothing needs to.
	//
	// Clearing SMTP_HOST on an instance that had one breaks that. The mailer becomes
	// nil, so the drain does not run; the rows enqueued before the change stay
	// pending; and the purge skips them by design. They are then invisible — nothing
	// but CountPendingMail reads them — and permanent (F52).
	//
	// Failing rather than deleting, so the record of what was attempted survives its
	// retention window like every other finished row, and reaches PurgeFinishedMail
	// by the normal path rather than by a second delete. The body goes for the same
	// reason MarkMailFailed drops it: a message that will never be sent should not
	// keep holding what it was going to say.
	//
	// Bounded by the same window rather than run eagerly, because an operator who
	// clears SMTP_HOST by mistake and puts it back the same afternoon should still
	// get their queue delivered. Only the caller decides when this applies: it runs
	// on the no-mailer path and nowhere else.
	AbandonUnsendableMail(ctx context.Context, maxAgeDays int32) (int64, error)
	// The replay guard, applied as a write rather than as a check.
	//
	// `mfa_last_step < @step` is the whole mechanism. A code from a step already
	// accepted updates no rows, and the caller reads zero as *refused*, so two
	// requests presenting the same code race each other into the same statement and
	// exactly one wins. Doing it as a read-then-write would leave a window in which
	// both saw the old value, which for a replay guard is the only window that
	// matters.
	//
	// `mfa_last_step IS NULL` is the first acceptance after an enrolment that
	// pre-seeded nothing, and is kept for completeness — `EnableUserMFA` stamps the
	// enrolling step, so in practice the column is never NULL while a secret exists.
	AcceptMFAStep(ctx context.Context, arg AcceptMFAStepParams) (int64, error)
	// Record the answer given at the first administrative sign-in after an upgrade
	// (D164).
	//
	// **Conditional on the question still being open**, so this is a first answer
	// and never a change of one. Two things fall out of that and both are the point:
	// two browser tabs racing produce one answer and one no-op rather than
	// last-write-wins, and the route cannot become the instance-settings page D161
	// refused to build — there is no second answer to give through it.
	//
	// Row count 1 means this caller's answer is the one that landed. Zero means the
	// question was already answered, by them or by somebody else holding
	// `instance.admin`, and the caller's own read has already established that the
	// row exists.
	AnswerUpdateCheck(ctx context.Context, enabled bool) (int64, error)
	ArchiveLink(ctx context.Context, arg ArchiveLinkParams) (Link, error)
	//
	// The archive an automation performs. Idempotent: a link already archived comes
	// back unchanged rather than erroring, so a rule whose window overlapped an
	// interactive archive does not fail its whole firing over it.
	//
	// Not `ArchiveLink`, and the difference is the missing actor. Every interactive
	// archive is authorized against a signed-in identity in internal/link; this one
	// is authorized by the rule, which was itself created by somebody holding
	// `automation.write`. Scoped to the workspace in the statement, so a rule can
	// only ever reach its own tenant's links.
	//
	// **`expires_at` is not touched**, and that is load-bearing rather than
	// incidental: moving it would make the link-expired trigger match this link
	// again on the next run, which is the self-feeding cycle the vocabulary is
	// arranged to make impossible.
	ArchiveLinkByAutomation(ctx context.Context, arg ArchiveLinkByAutomationParams) (ArchiveLinkByAutomationRow, error)
	AttachTag(ctx context.Context, arg AttachTagParams) error
	// The predecessor's bars, carried onto its successor inside the rotation
	// transaction.
	//
	// **Without this a reach revocation is escapable by the credential it was aimed
	// at.** A bar names an `api_key_id`; rotation mints a new id; nothing copied the
	// rows. So the holder of a key an administrator had cut out of an organization
	// sent `POST /api/v1/api-keys/rotate` — authenticated by the key's own token,
	// with no session and no permission anywhere — and the successor resolved back
	// into that organization and was told about it again. Driven through the product
	// 2026-08-09, both halves: it acted there and it read there. That is the case the
	// whole mechanism exists for, because the credential a reach revocation is aimed
	// at is the one that is *not* in legitimate hands, and it is exactly the holder
	// of that credential who has a reason to rotate.
	//
	// **Both columns are copied rather than rewritten**, and that is the substance of
	// the statement rather than a detail of it.
	//
	//   * `revoked_at` is the predecessor's. It is when the reach was cut, and
	//     rotating does not move that moment. `now()` would date an administrator's
	//     act by the clock of whoever rotated, make an old bar read as this
	//     morning's, and hand the owner's key list (F178) a date that resets every
	//     time the credential is replaced — which is precisely the reading somebody
	//     evading the bar would want it to have.
	//   * `revoked_by` is the predecessor's. The bar is that administrator's
	//     statement and they are who answers for it; attributing it to the rotating
	//     actor would name a credential's holder as the author of a bar against
	//     itself. NULL is carried unchanged and is already a state the schema means:
	//     `revoked_by` is `ON DELETE SET NULL` exactly so a bar outlives the
	//     administrator's account.
	//
	// **The two cases that copy nothing do so by data, not by a branch here.** A
	// **pinned predecessor** has no rows to select — nothing writes a bar for a key
	// whose organization is its whole reach, because cutting that reach and revoking
	// the key are the same act (`revokeInOrganization` sends it to
	// `revokePinnedKey`). And a **pinned successor** is excluded by the join, which
	// reads the reach off the row `insertSuccessor` has just written rather than off
	// a parameter that could disagree with it: an account-wide key may rotate into a
	// pinned one, and copying bars onto that would leave rows no resolution path
	// reads and put *cut out of Acme* on a key pinned to Beta. It would also break
	// 04200's stated invariant that a pinned key never carries one.
	//
	// Nothing is re-derived. Which organizations are barred is not recomputed from
	// memberships or from anything else — the rows are the record, and a copy is the
	// only operation that cannot disagree with them.
	CarryAPIKeyReachRevocations(ctx context.Context, arg CarryAPIKeyReachRevocationsParams) error
	//
	// The compare-and-set that fires a rule. **This is the loop guard.**
	//
	// The watermark is advanced *before* the actions run, and only if it is still
	// exactly where the match query saw it. Two things follow, and both are
	// deliberate:
	//
	//   * A rule cannot fire twice for one subject. The window the next run reads
	//     starts after the subject that was just handled, so the match set that
	//     produced this firing can never be produced again.
	//   * A second replica that briefly believes it is the leader loses the race
	//     rather than duplicating the firing — the same reasoning D77 gives for
	//     claiming a webhook delivery with FOR UPDATE SKIP LOCKED under the advisory
	//     lock, and the same reason: an advisory lock is released the instant its
	//     holder dies, so a moment of overlap is possible and has to cost nothing.
	//
	// The trade this direction makes is that a process killed between the claim and
	// the actions loses that firing rather than repeating it. That is the right way
	// round for an instruction that archives links and sends events: a missed
	// notification is recoverable by looking, and a rule that archived the same
	// links twice would be one nobody could trust to run unattended.
	//
	// `IS NOT DISTINCT FROM` rather than `=`, so a rule whose watermark is NULL —
	// the timestamp half only reachable by a row written outside this product, the
	// subject half on every rule that has not fired since 03600 — compares correctly
	// instead of never matching.
	//
	// Both halves are set and both halves are compared. The pair is one position in
	// the match order — the last subject a firing handled — and a claim that moved
	// one half against a stale reading of the other would hand two racing replicas
	// two different positions for the same firing.
	ClaimAutomationRule(ctx context.Context, arg ClaimAutomationRuleParams) (int64, error)
	//
	// One batch of due mail, claimed rather than merely selected.
	//
	// The UPDATE is what makes the claim: it spends the attempt and leases the row
	// forward, in one statement, before anything is sent. Two consequences, and
	// both are the point:
	//
	//   * A process killed between claiming and sending leaves a row that comes
	//     back on its own when the lease expires, instead of one stuck pending.
	//   * A crash loop is bounded. Counting the attempt at send time would let a
	//     process that dies mid-send retry the same message forever.
	//
	// FOR UPDATE SKIP LOCKED inside the subquery keeps two drainers from claiming
	// the same row. Leadership already keeps a second replica out of this job, but
	// leadership is an advisory lock released when its holder dies, so a moment of
	// overlap is possible; skip-locked makes that moment cost nothing rather than
	// send a message twice.
	//
	// Ordered oldest first, so a backlog drains in the order it was queued.
	ClaimDueMail(ctx context.Context, arg ClaimDueMailParams) ([]ClaimDueMailRow, error)
	//
	// One batch of due deliveries, claimed rather than merely selected — the exact
	// shape ClaimDueMail uses, and for the same two reasons:
	//
	//   * The UPDATE spends the attempt and leases the row forward before anything
	//     is sent, so a process killed mid-delivery leaves a row that comes back on
	//     its own instead of one stuck pending.
	//   * A crash loop is bounded. Counting the attempt at send time would let a
	//     process that dies mid-send retry the same delivery forever.
	//
	// FOR UPDATE SKIP LOCKED inside the subquery is the claim mechanism this
	// milestone had to choose (see decisions.md). Leadership already keeps a second
	// replica out of the job, but leadership is an advisory lock released when its
	// holder dies, so a moment of overlap is possible; skip-locked makes that moment
	// cost nothing rather than deliver the same event twice.
	//
	// The webhook's URL and secret are joined in here rather than fetched per row:
	// the drainer needs both for every claimed delivery, and N+1 round trips to
	// assemble a batch of network calls is the wrong shape.
	ClaimDueWebhookDeliveries(ctx context.Context, arg ClaimDueWebhookDeliveriesParams) ([]ClaimDueWebhookDeliveriesRow, error)
	// Take the day's update check, if it is available to take.
	//
	// **The daily bound is this statement, not a ticker.** One UPDATE decides
	// whether the check may run and records that it did, so the bound is a property
	// of the instance rather than of one process's uptime: a replica restarted
	// every ten minutes reads a row that says the check already happened and
	// declines, where a bare timer would ask GitHub on every boot.
	//
	// It writes the timestamp *before* the request rather than after it, which is
	// what makes a failure cost one attempt instead of one per tick. The milestone
	// forbids a retry storm and this is where that is enforced; a check that fails
	// waits out the same day a check that succeeded does.
	//
	// **`IS TRUE` rather than a bare test, because the column has three states**
	// (D164). A bare `AND update_check_enabled` would already decline on NULL —
	// unknown is not true — so the behaviour is the same and the spelling is not:
	// *off while unanswered* is a decision this statement enforces, and a reader
	// should not have to recover it from SQL's three-valued logic to be sure it was
	// meant.
	//
	// Row count 1 means the caller holds the check. Zero means the operator turned
	// it off, or has not been asked yet, or somebody has already run it today, and
	// the caller does not need to know which — all three are "do nothing".
	ClaimUpdateCheck(ctx context.Context, arg ClaimUpdateCheckParams) (int64, error)
	// Moving the flag an untagged scan resolves through takes two statements and one
	// transaction (M50's reopening, D183).
	//
	// **Two rather than one, and the reason is the index rather than taste.**
	// `UPDATE … SET is_default = (id = $3)` over the whole link reads as the obvious
	// single statement, and `qr_codes_link_default_key` is a plain unique index,
	// which Postgres checks as each row version is written rather than at the end of
	// the statement. Such an update collides with itself whenever the scan reaches
	// the incoming default before the outgoing one — the same failure
	// `UPDATE t SET n = n + 1` has on a unique column. A partial index cannot be
	// declared DEFERRABLE, because only a constraint can and a constraint cannot be
	// partial, so the ordering is made explicit instead: clear, then set, inside the
	// transaction the service opens.
	//
	// The window between them holds a link with no default at all. It is invisible:
	// the transaction has not committed, so no reader outside it sees either write,
	// and inside it the only reader is the second statement.
	// The first half. Takes the flag off whichever row holds it, or off nothing.
	ClearDefaultQRCode(ctx context.Context, arg ClearDefaultQRCodeParams) (int64, error)
	// The orphan sweep, run hourly by the maintenance pass.
	//
	// **What is orphaned under a column, and what is not.** Removing a code, a
	// workspace or an organization takes its logos by cascade, and replacing one is
	// the single UPDATE above, so none of those can leave bytes behind. Deleting a
	// *link* can, and does: a link is soft-deleted with a purge deadline, so its
	// `qr_codes` rows survive the whole trash window while every read in this file
	// filters them out with `l.deleted_at IS NULL`. Those bytes are unreachable
	// through the product — the endpoint that would clear them answers 404 for a
	// deleted link — and they sit in the row and in every `pg_dump` until the purge
	// fires, which for a large backlog is several hourly runs away and for a row
	// the purge skips is longer still.
	//
	// This is what makes m50.5.md's claim *deleting the link removes its artefacts*
	// true rather than merely intended. The row itself is left alone: the trash
	// window exists so a link can be brought back by hand, and the artefact is the
	// thing deletion was asked to remove.
	//
	// Idempotent by construction — a second run matches nothing, because `logo IS
	// NOT NULL` is the predicate. Bounded like every other pass in that job, and
	// SKIP LOCKED so it can never block, or be blocked by, a concurrent write to
	// the same code.
	ClearOrphanedQRCodeLogos(ctx context.Context, batchSize int32) (int64, error)
	//
	// NULL rather than an empty bytea, because the schema has one spelling for "no
	// logo" and two would disagree the first time somebody wrote a zero-length one.
	//
	// **The style goes with it, in this statement rather than a second one**
	// (M50.6's second reopening). The upload forces error correction to H and the
	// removal puts it back, which is a style write — and a style write of its own
	// would be an upsert, so a `DELETE` landing between the two would find no row to
	// conflict with and **insert a fresh code**, slug and all. One statement keyed on
	// the id cannot do that: a row that is gone updates nothing. The caller passes
	// the style it read, unchanged, for a code that had no logo to begin with.
	ClearQRCodeLogo(ctx context.Context, arg ClearQRCodeLogoParams) (int64, error)
	// Spend one click of a one-time or max-click link's durable budget.
	//
	// **One statement, and that is the whole of the concurrency argument.** Two
	// requests for the last click of a one-time link arrive at the same instant on
	// different replicas; both reach here; Postgres serialises them on the row lock
	// the ON CONFLICT path takes, so the second one re-evaluates its WHERE against
	// the first one's committed value and matches nothing. A read-then-write in Go,
	// or even a SELECT ... FOR UPDATE followed by an UPDATE, would need a
	// transaction the caller could forget to open; there is no such transaction to
	// forget here because the statement is the transaction.
	//
	// The insert races too: two requests for the *first* click of the same link both
	// try to INSERT, one wins, the loser takes the DO UPDATE branch and is evaluated
	// against the winner's row. That is why the limit test lives in the conflict
	// clause rather than only in the VALUES.
	//
	// Returns no row when the budget is spent, which the caller reads as 410. That
	// is deliberately the same shape as "no such link": the caller has a snapshot
	// already and does not need this query to tell it the link exists.
	//
	// click_limit is 1 for a one-time link, max_clicks otherwise, the smaller of the
	// two when both are set. A limit below one can never match, which is correct: a
	// link nobody may follow.
	ConsumeClickBudget(ctx context.Context, arg ConsumeClickBudgetParams) (ConsumeClickBudgetRow, error)
	// Spend it. Single use, decided by the statement rather than by the caller, for
	// the third time in this file and for the same reason.
	ConsumeMFAPendingLogin(ctx context.Context, id uuid.UUID) (int64, error)
	// Spends every unconsumed token for one account.
	//
	// **One statement, called from both ends of the flow**, because the two needs
	// are the same statement and writing it twice would be two places for the
	// predicate to drift.
	//
	// Requesting a reset calls it to supersede whatever was outstanding, so a fresh
	// request takes the slot and the previous link stops working at the same
	// moment — the shape DeleteOutstandingRegistration has for registrations,
	// except consumed rather than deleted: a superseded reset is evidence somebody
	// asked to recover this account twice, and the purge is what removes it later.
	//
	// Completing a reset calls it to spend the token just used *and its siblings*,
	// because a recovery that leaves a second live token behind has recovered
	// nothing: whoever else requested one — including whoever the person is
	// recovering from — would still hold a working link to the account whose
	// password just changed.
	ConsumePasswordResets(ctx context.Context, userID uuid.UUID) (int64, error)
	// Spends a registration. Conditional on it still being unspent, so this could
	// not succeed twice even without the lock above; zero rows rolls the
	// transaction back.
	ConsumePendingRegistration(ctx context.Context, id uuid.UUID) (int64, error)
	CountAutomationRules(ctx context.Context, workspaceID uuid.UUID) (int64, error)
	CountCampaigns(ctx context.Context, workspaceID uuid.UUID) (int64, error)
	CountClickEvents(ctx context.Context, workspaceID uuid.UUID) (int64, error)
	//
	// How many of this workspace's registrations actually receive a destination
	// somebody typed. The `/feeds` disclosure is built on it (M45, F135).
	//
	// **The predicate is the fan-out's predicate, deliberately.**
	// EnqueueWebhookDeliveries below queues a row for `w.enabled AND event =
	// ANY(w.events)`, so asking the same two conditions with the destination-carrying
	// events as the set is asking *would anything have been queued* rather than
	// guessing at it. A disclosure built on a looser test would warn about a
	// registration that receives nothing; one built on a tighter test would reassure
	// a workspace whose URLs are being posted somewhere.
	//
	// The event names come from the caller (domain.WebhookDestinationEvents) rather
	// than being written out here, because which payloads carry a destination is a
	// fact about internal/link's payload builders and must not be restated in a
	// second place that can drift from them.
	//
	// Cost: the same partial index the fan-out uses, `webhooks_workspace_idx ...
	// WHERE enabled` (00600), over at most MaxWebhooksPerWorkspace rows. It is read
	// on a dashboard page and on GET /api/v1/feeds, neither of which is the redirect
	// path.
	CountDestinationWebhooks(ctx context.Context, arg CountDestinationWebhooksParams) (int64, error)
	CountFolders(ctx context.Context, workspaceID uuid.UUID) (int64, error)
	// Only issued when the caller explicitly asks for a total, because counting
	// costs a scan the common page load should not pay for.
	CountLinks(ctx context.Context, arg CountLinksParams) (int64, error)
	// What deletion is refused for. Zero on every registered hostname today, because
	// nothing serves one and links are created on the default domain — the guard is
	// here so that it is already true when M40 makes it reachable.
	CountLinksOnDomain(ctx context.Context, domainID uuid.UUID) (int64, error)
	// Whether the person at this address is already in this organization.
	//
	// Asked at creation, where the actor holds members.write on the organization
	// and could read its member list anyway, so answering it discloses nothing they
	// could not already see. Redemption asks the same question of a user id it has
	// already resolved, and answers it with the same generic refusal as every other
	// failure.
	CountMembershipsForEmail(ctx context.Context, arg CountMembershipsForEmailParams) (int64, error)
	CountMembershipsForUser(ctx context.Context, arg CountMembershipsForUserParams) (int64, error)
	//
	// The other re-notify guard, and it is keyed on the thing rather than on the
	// clock (M55).
	//
	// CountRecentNotificationsOfKind above suppresses a warning that is *still
	// true* — the audit log is still too big — so it asks "was this said lately".
	// A release is a different shape: the answer is not that it was said lately, it
	// is that this exact version has already been reported and reporting it again
	// says nothing new. So the version is the key, and there is no window: an
	// operator who was told about 0.4.0 a year ago is not told again, and 0.5.0 is
	// a new fact that arrives once.
	//
	// Reading `data->>'version'` rather than a column of its own is deliberate. The
	// notification is the record that the operator was told; a column beside it
	// would be a second place for the same fact, and the two would disagree the
	// first time one write succeeded and the other did not.
	CountNotificationsAboutVersion(ctx context.Context, arg CountNotificationsAboutVersionParams) (int64, error)
	// What the queue's heading says there is to do. Served by the partial unique
	// index, whose predicate this matches exactly.
	CountOpenDestinationDisputes(ctx context.Context) (int64, error)
	// What D37 refuses an organization deletion on, and it is deliberately the same
	// shape as CountWorkspaceLinks one level up.
	//
	// Archived links count, soft-deleted ones do not — the reasoning is D32's and is
	// written out there. What is new here is *why the org level asks the question at
	// all*: cascading through these links would make D32 bypassable by deleting one
	// level up, which turns a rule into a speed bump. So the organization refuses on
	// exactly the rows its workspaces would refuse on.
	CountOrganizationLinks(ctx context.Context, organizationID uuid.UUID) (int64, error)
	// Whether this is the organization's last workspace.
	//
	// Deleting it would leave every member of the organization resolving into no
	// workspace at all, which `ResolveWorkspaceForUser` reports as a broken instance
	// rather than as an empty state — so the account could not authenticate. Guarded
	// for the same reason the last owner is.
	CountOrganizationWorkspaces(ctx context.Context, organizationID uuid.UUID) (int64, error)
	CountPendingMail(ctx context.Context) (int64, error)
	CountPendingWebhookDeliveries(ctx context.Context) (int64, error)
	// What domain.MaxQRCodesPerLink is checked against, the way campaign creation
	// checks CountCampaigns.
	CountQRCodes(ctx context.Context, arg CountQRCodesParams) (int64, error)
	//
	// The re-notify guard. A threshold that is still crossed is still crossed on
	// the next run an hour later, and a notification per hour forever is how an
	// inbox becomes something people stop reading — which would cost exactly the
	// warning D5 depends on.
	CountRecentNotificationsOfKind(ctx context.Context, arg CountRecentNotificationsOfKindParams) (int64, error)
	// Read before an insert, to enforce the per-link ceiling. Counts every kind,
	// not only 'match': the ceiling exists because the whole list travels inside
	// the cached snapshot and is walked in order on the redirect path, and M36's
	// rows will be in that list too.
	CountRoutingRules(ctx context.Context, linkID uuid.UUID) (int64, error)
	//
	// Served by notifications_user_unread_idx, the partial index the table already
	// ships with: the WHERE clause here has to match the index's predicate exactly
	// or this becomes a sequential scan on every page render in the dashboard.
	//
	// **Both halves of the workspace predicate are load-bearing** (D102, F105).
	// `workspace_id = @workspace_id` alone hides every organization-level
	// notification, because disputes and audit-growth write NULL — the reader would
	// lose exactly the notifications that are not about any one workspace. And the
	// clause has to be identical here and in ListUnreadNotificationPreview below, or
	// the badge and the list it previews disagree while one of them stops using the
	// index.
	CountUnreadNotifications(ctx context.Context, arg CountUnreadNotificationsParams) (int64, error)
	// What the account page shows. A number somebody acts on: three left is a prompt
	// to regenerate, and zero with a lost phone is a conversation with the operator.
	CountUnusedMFARecoveryCodes(ctx context.Context, userID uuid.UUID) (int64, error)
	// Whether an account belongs to anything at all.
	//
	// Read by exactly one caller: the first-organization path (D36). An account with
	// no membership holds no role and therefore no `orgs.create`, so without this
	// the prompt to create an organization would lead somewhere it is refused. This
	// is a check on **present state** — how many memberships exist right now — and
	// not on how the account was made, which is the distinction D16 was drawing when
	// it made the permission a grant rather than a provenance test.
	CountUserMemberships(ctx context.Context, userID uuid.UUID) (int64, error)
	// Users, sessions and tenancy provisioning.
	// Drives the first-run setup flow: /setup exists only while this is zero.
	CountUsers(ctx context.Context) (int64, error)
	// Whether an address already has an account, for the signup form.
	//
	// Counted rather than selected: the caller needs the answer and nothing else,
	// and returning the row would put somebody else's name and hash in a variable
	// that only ever gets compared against zero.
	CountUsersByEmail(ctx context.Context, email string) (int64, error)
	CountWebhooks(ctx context.Context, workspaceID uuid.UUID) (int64, error)
	// How many hostnames this workspace has registered, which is what the
	// per-workspace cap is applied to (M40, reopened).
	//
	// Every undeleted row, verified or not, because what the cap bounds is the work
	// a registration creates: one outbound DNS lookup per hostname per pass, against
	// a nameserver the registrant chooses. An unverified hostname costs exactly the
	// same lookup as a verified one, so counting only the verified ones would bound
	// the wrong number.
	CountWorkspaceDomains(ctx context.Context, workspaceID *uuid.UUID) (int64, error)
	// What D32 refuses a workspace deletion on.
	//
	// Soft-deleted links are excluded on purpose. `links`, `tags` and `folders` all
	// cascade from `workspaces`, so the guard is in front of a real cascade — but a
	// link the owner already deleted is one they cannot delete again, and counting
	// it would leave the workspace undeletable until the purge job ran, with nothing
	// the person could do about it. Archived links **are** counted: an archived link
	// keeps its alias and its click history, so cascading it away would be silent
	// data loss dressed as tidying up.
	CountWorkspaceLinks(ctx context.Context, workspaceID uuid.UUID) (int64, error)
	// API keys and the permission vocabulary their scopes are drawn from.
	CreateAPIKey(ctx context.Context, arg CreateAPIKeyParams) (ApiKey, error)
	// Automation rules and their evaluation (M43).
	//
	// Two halves that never meet in one statement, the shape webhooks.sql already
	// has. The rule half is workspace-scoped and reached from the dashboard and the
	// API; the evaluation half is reached only by the scheduler and deliberately
	// carries no workspace parameter in its first query — a run that filtered by
	// tenant would evaluate in tenant order, and the fairness this needs is
	// least-recently-looked-at first across the instance.
	//
	// **`last_fired_at` is a watermark, not a diagnostic — and it is half of one.**
	// Every match query below orders by (event time, id) and takes the window that
	// opens strictly after the pair `(@after, @after_subject)` and closes at
	// `@until`. The pair, not the instant alone: a capped fetch can stop part-way
	// through subjects sharing one timestamp, and a lower bound that carries only
	// the timestamp cannot re-enter that tie group — it either skips the tied
	// remainder forever (strict `>`) or re-fires what was already handled (`>=`).
	// `last_fired_subject_id` (03600) is the id half. What the watermark is *for* is
	// unchanged: a subject is matched once, the watermark moves past it, and no
	// later run can see it again. Removing the advance turns every rule into a
	// runaway that fires on the same subject on every tick.
	//
	// **`last_checked_at` is the scheduler's cursor, and it is a different fact.**
	// When a rule was last *looked at*, whether or not it fired. It orders the due
	// query and bounds no window; the watermark bounds every window and orders
	// nothing. Ordering on the watermark is what F83 was — it moves only when a rule
	// fires, so an idle rule held the head of the queue permanently and the
	// hundred-and-first enabled rule on an instance was never evaluated at all
	// (03100).
	//
	// `last_fired_at` is set at creation rather than left NULL. A NULL watermark on
	// a rule created today would mean "every link that ever expired", so the first
	// run of a brand-new rule would fire for the entire history of the workspace.
	//
	// `last_checked_at` is left NULL and travels back with the row, as it does in
	// every statement below. NULL sorts first in the due query, so a rule somebody
	// just wrote is looked at on the next tick instead of waiting for its turn — and
	// carrying the column in the projection is what keeps these four statements
	// returning the table's own row type rather than four near-identical structs.
	//
	// `last_fired_subject_id` is left NULL for the same projection reason and one of
	// its own: a rule that has never fired has no boundary subject, and NULL is read
	// by the evaluator as "the arming instant is fully spent" — the same strict `>`
	// a timestamp-only watermark meant.
	CreateAutomationRule(ctx context.Context, arg CreateAutomationRuleParams) (AutomationRule, error)
	// Campaigns and QR codes (M41).
	//
	// Both tables were created dormant by 00600 and woken by 02700. They share this
	// file because they share a milestone and neither is large enough to be worth
	// its own; nothing else connects them.
	//
	// **A campaign is soft-deleted and a QR style is not.** The asymmetry is
	// deliberate. A campaign names a body of work whose links keep their history
	// after it ends, and `links.campaign_id` is ON DELETE SET NULL, so a hard delete
	// would unlabel every link the moment somebody tidied up a finished campaign —
	// exactly the failure the folder migration (02400) argues against. `deleted_at`
	// keeps the row, and every statement here filters on it. A QR style is a
	// rendering preference with nothing to restore: deleting it returns the link's
	// code to the default style, which is the state every link starts in.
	CreateCampaign(ctx context.Context, arg CreateCampaignParams) (Campaign, error)
	CreateDestination(ctx context.Context, arg CreateDestinationParams) (Destination, error)
	// Domain ownership and registration (M39).
	//
	// The settings queries for the instance default live in links.sql, where they
	// were written when there was exactly one domain. These are the ones that exist
	// because there can now be more than one, and every statement here reads or
	// writes the *ownership* columns rather than the serving ones — nothing on a
	// registered hostname is served until M40 verifies it.
	//
	// Ownership is never decided in SQL. Every write below is by id, and
	// link.Service reads the row first and judges the actor against it, so the
	// refusal is a sentence naming whose domain it is rather than a statement that
	// silently affected no rows. ListDomains is the exception, and it is a read: it
	// is scoped by the actor's organization and workspace because a list is only
	// ever the caller's own.
	// Registered, and deliberately unverified: verified_at stays NULL and
	// ssl_status stays at its 'none' default. is_default is never set here — there
	// is one instance default and 00700 seeded it.
	//
	// The challenge token is minted here (M40) rather than lazily on the first
	// verification attempt, so the page that tells somebody which DNS record to
	// publish can do it the moment they register — the alternative is a page that
	// asks them to come back.
	// **The bot policy is inherited from the instance default (F89).** It is
	// instance-wide, and a hostname registered after the operator turned blocking on
	// would otherwise start at the column defaults and reopen the hole propagating
	// the setting closes. Read from the default row rather than passed in, so there
	// is no argument a caller can get wrong and no second place the two settings can
	// disagree. Both subqueries read the same row; if the seeded default were ever
	// missing they COALESCE to false together, which is a state the CHECK accepts.
	CreateDomain(ctx context.Context, arg CreateDomainParams) (Domain, error)
	// Folders (M38).
	//
	// **There is no recursive SQL here, and that is a decision rather than an
	// omission.** A folder tree is naturally a WITH RECURSIVE walk, and the walk was
	// written first; it was replaced by ListFolders, which reads the workspace's
	// folders flat and lets internal/link assemble the tree in Go. Three reasons,
	// in the order they matter:
	//
	//   - The interesting rules are not "who are my children". They are "may this
	//     move happen" — a folder may never become its own descendant — and "how
	//     deep would the result be". Both are walks over the same set, and running
	//     them as three more recursive CTEs would put the milestone's two named
	//     failure modes in three places where only integration tests can reach them.
	//     In Go they are one function with a unit test that does not need Postgres.
	//   - The set is small by construction. The depth cap is
	//     domain.MaxFolderDepth, the sibling-name rule stops a tree fanning out by
	//     accident, and this is a structure a person curates by hand.
	//   - A recursive CTE over a table with no cycle constraint runs forever if the
	//     data ever holds one. The Go walk carries a visited set and stops.
	//
	// Deleting a folder is a real DELETE (see migration 02400): `parent_id ON DELETE
	// CASCADE` takes the subtree and `links.folder_id ON DELETE SET NULL` unfiles
	// every link in any part of it. Every statement below still filters on
	// `deleted_at IS NULL`, so the partial indexes serve them and a later decision
	// to soft-delete does not silently resurrect rows.
	CreateFolder(ctx context.Context, arg CreateFolderParams) (Folder, error)
	// Invitations: issuing, listing, revoking and redeeming (M27).
	CreateInvitation(ctx context.Context, arg CreateInvitationParams) (Invitation, error)
	// Links, destinations and tags.
	CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error)
	// The credential the browser holds between a right password and a session.
	//
	// Returned in full so the caller can assert the expiry it asked for rather than
	// recompute it from its own clock — the TTL m53.md wants a test to hold is the
	// one the database wrote.
	CreateMFAPendingLogin(ctx context.Context, arg CreateMFAPendingLoginParams) (MfaPendingLogin, error)
	CreateMembership(ctx context.Context, arg CreateMembershipParams) (Membership, error)
	CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
	// Account recovery: the reset tokens a forgotten password is repaired with
	// (M51). The account's own row is written through query/auth.sql's
	// UpdateUserPassword, so there is one password-writing statement in the product
	// and not two.
	CreatePasswordReset(ctx context.Context, arg CreatePasswordResetParams) (PasswordReset, error)
	// Self-serve signup: the registrations waiting on an address to be proven
	// (M29). The mode itself is `LINKCTRL_SIGNUP_MODE` and is never read from the
	// database (D38), so nothing here answers what the instance admits.
	CreatePendingRegistration(ctx context.Context, arg CreatePendingRegistrationParams) (PendingRegistration, error)
	// Routing rules (M34).
	//
	// Every query here filters on kind = 'match'. That is not defensive coding: the
	// column's CHECK also permits weighted, sequential and fallback, which are
	// M36's, and a query that read every kind would start behaving differently the
	// day those rows first exist — silently, on the redirect path. The filter means
	// M36 has to write its own reads, which is the correct amount of work for a
	// milestone that adds a new evaluation model.
	//
	// Rule *targets* are ordinary `destinations` rows. A rule therefore costs two
	// writes and the destination is what carries the URL, its host, and the M30
	// tier check the service applied before either row existed.
	CreateRoutingRule(ctx context.Context, arg CreateRoutingRuleParams) (RoutingRule, error)
	// A rule's target. Position is above zero so it can never be mistaken for the
	// link's own destination, which Phase 1 put at position 0 and which
	// `links.primary_destination_id` points at.
	CreateRuleDestination(ctx context.Context, arg CreateRuleDestinationParams) (Destination, error)
	// ON CONFLICT DO NOTHING returns no row when another replica inserted first,
	// which the caller detects and re-reads. Two replicas using different salts
	// for the same day would split every visitor in two.
	CreateSalt(ctx context.Context, arg CreateSaltParams) ([]byte, error)
	// workspace_id is written at sign-in rather than left for the first switch, so
	// a session says where it is from its first row. Resolution would answer the
	// same either way — a NULL simply falls through to the user's preference — but
	// a switcher that only takes effect after the first switch is a switcher whose
	// state is unreadable until somebody uses it.
	CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
	// --- tags -------------------------------------------------------------------
	CreateTag(ctx context.Context, arg CreateTagParams) (Tag, error)
	CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
	// A variant's target, with its weight. The same row CreateRuleDestination
	// writes, plus the one column that makes it an arm rather than a target: 00300
	// has carried `weight` since Phase 1 with a comment naming this milestone.
	CreateVariantDestination(ctx context.Context, arg CreateVariantDestinationParams) (Destination, error)
	// --- split testing (M36) -----------------------------------------------------
	//
	// A variant is a rule row of kind weighted, sequential or fallback, pointing at
	// its own `destinations` row exactly as a match rule's target does. So the
	// writes below are the same two writes, and the M30 tier check the service
	// applied before either row existed is the same check.
	//
	// Every query here excludes `match` for the reason every query above requires
	// it: the two management surfaces address disjoint sets of rows, so a rule id
	// handed to the wrong endpoint finds nothing rather than editing a rule of a
	// kind the caller was not looking at.
	// Priority is fixed at the column default and never read for a variant: arms are
	// chosen, not matched in order, and a priority on one would be a number the
	// dashboard shows and nothing obeys. Creation order is what orders a rotation,
	// which is what ListVariantRules sorts by.
	CreateVariantRule(ctx context.Context, arg CreateVariantRuleParams) (RoutingRule, error)
	// Webhooks and their delivery queue (M42).
	//
	// Two halves that never meet in one statement. The registration half is
	// workspace-scoped and reached from the dashboard and the API; the delivery half
	// is scoped to nothing but time and is reached only by the scheduler. Every
	// query below belongs to exactly one of them, and the delivery half deliberately
	// carries no workspace parameter — a drainer that filtered by tenant would
	// deliver in tenant order, and the queue is fair or it is a queue somebody's
	// backlog can starve.
	CreateWebhook(ctx context.Context, arg CreateWebhookParams) (CreateWebhookRow, error)
	CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, error)
	// Records a decision, and only on a dispute nobody has decided yet.
	//
	// The `status = 'open'` predicate is the concurrency control: two owners
	// clicking allow and uphold on the same row produce one decision and one
	// no-rows, rather than a last-writer-wins that leaves the audit record and the
	// blocklist disagreeing about what happened.
	DecideDestinationDispute(ctx context.Context, arg DecideDestinationDisputeParams) (DestinationDispute, error)
	// Everything hanging off the account that must not outlive it, removed in one
	// statement and counted.
	//
	// **Written out because a soft delete fires no foreign key.** All eight tables
	// below declare `ON DELETE CASCADE` against `users`, and every one of those
	// clauses triggers on `DELETE`; the account row is kept — that is what
	// `anonymized_at` marks and what the partial `users_email_key` is shaped for —
	// so the cascade never runs and these statements are what stands in for it.
	//
	// Four of them are the tables M52 enumerates: `memberships`, `sessions`,
	// `api_keys`, `notifications`. Four more are here because leaving them would
	// falsify a claim the schema already makes:
	//
	//   * `password_resets`, whose own comment (03900) says *"there is no route by
	//     which a reset for a deleted account could still be consumed, because the
	//     row is gone with the account"*. Under a soft delete the row is not gone,
	//     and it is the one credential in this schema that sets a password.
	//   * `instance_grants`, whose own comment (03400) says a grant naming a user
	//     who does not exist *"is not a record worth keeping, it is a permission
	//     nobody can hold"*. The instance principal cannot reach this statement at
	//     all — deleting it is refused — but a delegated dispute reviewer can.
	//   * `mfa_recovery_codes` and `mfa_pending_logins` (04100), added by M53 and
	//     added *by* M53 rather than deferred, because M53 is what creates them: a
	//     recovery code is a standing credential that admits somebody to an account
	//     with no password, and a pending login is one that mints a session. Both
	//     are the `password_resets` defect in a new table, and shipping the tables
	//     without the statements would have reintroduced it in the same phase that
	//     closed it.
	//
	// The counts come back so the caller can log what went, and so a test can assert
	// the statement reached each table rather than assert it did not error.
	DeleteAccountDependents(ctx context.Context, accountID uuid.UUID) (DeleteAccountDependentsRow, error)
	DeleteAutomationRule(ctx context.Context, arg DeleteAutomationRuleParams) (int64, error)
	// Removes one host from the low-confidence runtime list.
	//
	// The only deletion in this program that is not a reconciliation, and the only
	// one an `allow` decision performs. Scoped to an exact host — the row the
	// dispute recorded when it was filed and the queue displayed on the button — so
	// a decision about 'login.evil.example' cannot take 'evil.example' off the list
	// by accident. It can take it off deliberately, and routinely does: 'evil.example'
	// is what refused 'login.evil.example', and lifting anything else would leave the
	// destination refused. What 03300 changed is that the owner is now told which of
	// the two they are deciding about, and that the answer cannot move between the
	// filing and the click.
	//
	// It cannot reach the other two tiers, and there is nothing to scope against
	// them: the embedded list is a compiled file and the unappealable tier has no
	// row anywhere. That is the structural half of "decisions act only on the
	// runtime low-confidence list".
	DeleteBlockedDestination(ctx context.Context, host string) (int64, error)
	// Soft. The links keep their history; `links.campaign_id` is cleared by the
	// statement below rather than by a cascade, because a cascade would fire on a
	// hard delete this statement never performs.
	DeleteCampaign(ctx context.Context, arg DeleteCampaignParams) (int64, error)
	// Reaper. Revoked rows are kept briefly so "sign out everywhere" is visible in
	// the session list before it disappears.
	DeleteExpiredSessions(ctx context.Context) (int64, error)
	// A real DELETE. The two foreign keys 00300 wrote are what make it safe:
	// descendants cascade, and every link in the subtree has its folder_id set to
	// NULL. Nothing here touches `links`, and nothing here may: the moment this
	// statement grows a link delete, deleting a container starts deleting content.
	DeleteFolder(ctx context.Context, arg DeleteFolderParams) (int64, error)
	// Every outstanding pending login for an account.
	//
	// Two callers. A fresh password post supersedes whatever was outstanding, so there
	// is never more than one live prompt per account and somebody who abandoned a tab
	// is not sharing their window with it. And disabling the second factor takes them
	// all, because a prompt that outlives the factor it was prompting for is a
	// credential with nothing left to check.
	DeleteMFAPendingLoginsFor(ctx context.Context, userID uuid.UUID) (int64, error)
	// Every code for an account, spent ones included.
	//
	// Both callers want exactly this. Regenerating voids the previous set in full, so
	// keeping the spent rows would leave a count of leftovers from a set that no
	// longer opens anything. Disabling removes the account's last credential of this
	// kind, and m53.md names it in the same breath as clearing the secret.
	DeleteMFARecoveryCodes(ctx context.Context, userID uuid.UUID) (int64, error)
	DeleteMembership(ctx context.Context, arg DeleteMembershipParams) (int64, error)
	// A real delete, not a soft one, and everything in 00200/00300/00500/01200 that
	// references it cascades: workspaces and everything under them, memberships,
	// invitations, API keys, and any custom domain.
	//
	// Two things deliberately do not. `audit_logs.organization_id` carries no
	// foreign key, so the trail this organization wrote survives the row it
	// describes — a tenancy teardown that erased its own record is the one shape an
	// audit log must not have. And the instance default domain has
	// `organization_id IS NULL`, so it and the `reserved_aliases` rows keyed to it
	// are untouched.
	DeleteOrganization(ctx context.Context, id uuid.UUID) (int64, error)
	//
	// The three analytics rollups belonging to an organization's workspaces.
	//
	// They carry `workspace_id` with no foreign key — a deliberate choice recorded
	// at 00400, so a rollup job never blocks on a tenancy write and a partition drop
	// never has to consider them — and the cost of that choice is that nothing
	// cascades them. `link_click_daily`, `link_dimension_daily` and
	// `workspace_click_daily` therefore outlived the tenancy they describe, while
	// DeleteOrganization's own doc said the audit trail was all that survived (F106).
	//
	// Run *before* the organization is deleted, and that ordering is required rather
	// than tidy: the workspaces are what name these rows, and the cascade takes the
	// workspaces. After the delete there is nothing left to select them by.
	//
	// Not a security fix. Every reader scopes to a live workspace_id, so these rows
	// are unreachable rather than exposed; what they are is stale aggregate data
	// with no owner, and a sentence that was not true.
	DeleteOrganizationRollups(ctx context.Context, organizationID uuid.UUID) (int64, error)
	// Clears whatever is outstanding for an address so a fresh attempt can take the
	// slot.
	//
	// Superseding rather than refusing, because the ordinary reason somebody
	// registers twice is that the first mail never arrived. The old token stops
	// working at the same moment, which is what makes this safe: there is never
	// more than one live link per address.
	DeleteOutstandingRegistration(ctx context.Context, email string) (int64, error)
	// Removes one code. Scoped by workspace rather than by link, because the id is
	// already unique and the service has resolved the link before it gets here; the
	// workspace column is the tenancy check.
	//
	// **`AND slug <> ”` is gone** (D183). It was what refused to delete the default
	// code, back when the default *was* the empty slug; the refusal that replaces it
	// is the service's, and it is about arithmetic rather than identity — a link's
	// last code cannot be removed, whichever one it is.
	//
	// The logo goes with the row, which is the whole of what D134 bought: no second
	// statement, and no way for the two to come apart.
	DeleteQRCodeByID(ctx context.Context, arg DeleteQRCodeByIDParams) (int64, error)
	// Reaper. Kept long enough to be visible in the key list after revocation, and
	// long enough for the audit question above to be answerable.
	DeleteRevokedAPIKeys(ctx context.Context) (int64, error)
	// Returns the destination so the caller can remove it in the same transaction.
	// A rule target left behind would be an orphan row nothing reads and nothing
	// can reach, accumulating one per deleted rule for the life of the link.
	DeleteRoutingRule(ctx context.Context, arg DeleteRoutingRuleParams) (*uuid.UUID, error)
	// A hard delete, not the soft delete `deleted_at` exists for. Nothing reports
	// on a rule target and nothing can restore a deleted rule, so a tombstone here
	// would be a row that only ever grows the table.
	// The NOT EXISTS is a guard rather than a branch anything takes: only rule
	// targets reach this query. It is here because the one row this must never
	// delete is the link's own destination, and the cost of being wrong about that
	// is a link that redirects nowhere.
	DeleteRuleDestination(ctx context.Context, arg DeleteRuleDestinationParams) error
	// Retires environment entries the operator has since removed.
	//
	// Scoped to source = 'env' and nothing else. A restart must never delete what an
	// owner decided in the review queue, nor the shortener hosts seeded by
	// migration, which is the one way a boot-time reconciliation could quietly undo
	// a decision somebody made.
	DeleteStaleEnvBlockedDestinations(ctx context.Context, keep []string) (int64, error)
	DeleteTag(ctx context.Context, arg DeleteTagParams) (int64, error)
	DeleteVariantRule(ctx context.Context, arg DeleteVariantRuleParams) (*uuid.UUID, error)
	//
	// The deliveries go with it, by the ON DELETE CASCADE 00600 declared. A webhook
	// that has been removed and a delivery log that still names it would be a record
	// of where events *used to* go, which is a different and less useful thing than
	// the record of where they go.
	DeleteWebhook(ctx context.Context, arg DeleteWebhookParams) (int64, error)
	// A real delete, not a soft one, and that is the decision D32 guards.
	//
	// `links`, `tags` and `folders` cascade from here (00300_links.sql). Soft
	// deleting instead would leave those rows behind and their aliases still
	// serving redirects out of a workspace the dashboard says is gone, which is a
	// worse outcome than the cascade — so the guard goes in front of the delete and
	// the delete is honest about what it does.
	DeleteWorkspace(ctx context.Context, arg DeleteWorkspaceParams) (int64, error)
	DetachAllTags(ctx context.Context, linkID uuid.UUID) error
	// Taking the second factor away.
	//
	// Everything at once, because m53.md asks for exactly that: *clearing
	// `mfa_enabled_at` clears the secret and every unused recovery code in the same
	// transaction*. The codes are the caller's second statement — this one is the
	// account row — and both are in one transaction, which is what makes "the account
	// has no second factor" a state with no intermediate.
	//
	// `mfa_last_step` goes too. It is meaningless without a secret, and leaving it
	// would mean a later enrolment inherited a replay floor from a secret that no
	// longer exists — an account that re-enrolled would find its first codes refused
	// until the clock caught up.
	DisableUserMFA(ctx context.Context, userID uuid.UUID) (int64, error)
	// Enrolment, committed.
	//
	// **`mfa_enabled_at` and `mfa_secret` are written together and only together**,
	// which is m53.md's *half-enrolled is not a state this product has* stated as a
	// single UPDATE. The secret is not parked on the row while the person fetches
	// their phone: it is held by the enrolment session and reaches the database only
	// in the statement that also says the second factor is on, and only after a code
	// computed from it has verified.
	//
	// `mfa_enabled_at IS NULL` in the predicate rather than checked beforehand, so
	// enrolling an account that is already enrolled affects no rows instead of
	// silently replacing a working secret with a different one. Two tabs finishing the
	// same enrolment is the ordinary way that happens.
	EnableUserMFA(ctx context.Context, arg EnableUserMFAParams) (int64, error)
	// The mail outbox. Queued on the request path, drained by the scheduler.
	//
	// The message is stored rendered. Nothing here re-renders from a template, so a
	// later template change cannot rewrite a mail somebody is already waiting for.
	EnqueueMail(ctx context.Context, arg EnqueueMailParams) error
	// --- the queue ---------------------------------------------------------------
	//
	// One statement fans one event out to every webhook that asked for it.
	//
	// This runs inside a link write, so its cost on a workspace with no webhooks has
	// to be one indexed lookup that returns nothing — which is what the partial index
	// `webhooks_workspace_idx ... WHERE enabled` (00600) makes it.
	//
	// The payload is rendered by the caller and stored rendered, for the reason the
	// mail outbox stores its body rendered (D23): a change to what an event looks
	// like must not rewrite an event that was already queued, and a row has to stay
	// readable after the code that produced it is gone.
	//
	// gen_random_uuid() rather than a v7 generated in Go, because the number of rows
	// is not known until the SELECT runs. Nothing orders deliveries by id — the queue
	// orders by next_attempt_at — so the time-sortability v7 buys is not spent here.
	EnqueueWebhookDeliveries(ctx context.Context, arg EnqueueWebhookDeliveriesParams) (int64, error)
	// Mint the secret if it is not there, and return whichever one is authoritative.
	//
	// COALESCE rather than a read-then-write, so two people asking for a signed URL
	// at the same moment cannot end up with signatures made under different keys:
	// the second UPDATE sees the first one's committed value and keeps it. The
	// caller generates the candidate bytes, because a random source belongs in the
	// application rather than in an extension this schema does not require.
	EnsureWorkspaceSigningSecret(ctx context.Context, arg EnsureWorkspaceSigningSecretParams) ([]byte, error)
	// The erasure pass. One batch, one statement, one transaction.
	//
	// **What it scrubs is what deletion could not reach**, and there are two ways a
	// row gets there. Two tables carry an address snapshot and **no** foreign key to
	// `users` at all, because a record of the past that vanishes with its subject is
	// not a record: `audit_logs` and `destination_disputes`. Two more do have one and
	// are still out of reach — `notifications` (`00600:127`) and `invitations`
	// (`01200:62`) — because the row belongs to a *different* person, so ending this
	// account was never going to remove it and no retention window expires it.
	//
	// *No foreign key* was the criterion when this pass scrubbed two tables. It is
	// not the criterion now and saying so was wrong for a while: what finds all four
	// is **a record about this person that ending the account does not remove**.
	//
	//   * `audit_logs.actor_label` (`00600:137,150`). An audit trail that vanishes
	//     with its actor is not an audit trail.
	//   * `destination_disputes.created_by_label` **and** `decided_by_label`
	//     (`01600:64,68`). Two snapshots, not one: an account is as identifiable as
	//     the moderator of a dispute as it is as the filer of one, and F44 names this
	//     as the second table with no deletion path of any kind.
	//   * `audit_logs.metadata`'s `"email"` key (F177). **Seven** writers, counted
	//     against the tree on 2026-08-09 rather than recalled: `invite.go:437`
	//     (`invitation.created`) and `:860` (`invitation.redeemed`),
	//     `team/member.go:197`, `:258` and `:386` (`member.role_changed`,
	//     `member.removed`, `member.added`), and `instance/instance.go:642`
	//     (`instance.principal_moved`) and `:677` (the review grants). The address
	//     there is usually the *subject* of somebody else's action, so scrubbing it
	//     edits a record whose actor is still here — weighed and taken, because an
	//     erasure that reaches the label and stops at the detail one column over has
	//     not erased the person.
	//
	//     Matched on the value, because there is no foreign key to match on: the
	//     column is jsonb and the address in it is a snapshot. Case-folded on both
	//     sides, since `invitation.created` stores what the administrator typed and
	//     that need not be the case the account was registered in. The accepted cost
	//     is a sequential scan of a partitioned table — the largest thing in this
	//     schema after analytics — paid once per batch, and only when there is a
	//     batch: see `HAVING count(*) > 0` on `batch` below, which is what makes an
	//     idle pass free rather than hourly.
	//
	//     Written by the **same** UPDATE as `actor_label` rather than by a second
	//     one, and that is not a tidiness choice — see the note on `batch`.
	//   * `audit_logs.metadata`'s `"from"` **array** (F189). One writer:
	//     `instance/instance.go:642` puts the outgoing principals' addresses in an
	//     array beside the `"email"` key it also writes, so a prior instance
	//     principal who later deletes their account kept an address one key over
	//     from the one that was scrubbed. The scalar predicate above is the only
	//     shape F177 specified and an array is a different one, which is why this
	//     was a second finding rather than an oversight in the first.
	//
	//     Rewritten element by element rather than dropped: the array says how many
	//     principals the role moved away from, and losing a member of it loses that
	//     count. Order is held by `WITH ORDINALITY`, because the addresses are the
	//     record of a single act and the sequence is part of what it says. A `from`
	//     that is not an array is left alone rather than erroring, so a record
	//     written by hand cannot fail the whole batch.
	//   * `notifications.data`'s `"email"` key **and the title beside it** (F188).
	//     `invite/invite.go:973` tells the inviter that their invitation was
	//     accepted, and both the detail and the sentence carry the address of the
	//     person who accepted. The row belongs to the *inviter*, so deleting the
	//     erased account's own notifications never reached it and nothing expires
	//     it — notifications are scoped to a reader, not swept by age.
	//
	//     The title is rewritten by `replace` against `data->>'email'` rather than
	//     against the batch, and that is what keeps this out of the business of
	//     knowing how the sentence is worded: the two came from one value at one
	//     call site, so the address in the title is exactly the string the detail
	//     holds. Both CASEs read the pre-update row, so the title still finds the
	//     address after the same statement has replaced it in `data`.
	//
	//     An **outstanding** invitation is left alone for the reason stated below,
	//     and this is deliberately the other answer: a notification is a record
	//     *about* a person delivered to somebody else, which is the same thing
	//     `audit_logs.metadata` is, and it gets the same treatment.
	//   * `invitations.email`, on the invitations the erased account **redeemed**
	//     (F181). `ListInvitations` carries no state predicate, so `/invites`
	//     renders every invitation an organization ever issued — redeemed ones
	//     included — and the address each was sent to. Nothing deletes those rows
	//     and no setting expires them, so an account deleted, erased and tombstoned
	//     everywhere else was still named in full on an ordinary dashboard page.
	//
	//     `redeemed_by` is the join, not the address. What is scrubbed is the row
	//     this account *joined by*, which is a record about them; an **outstanding**
	//     invitation addressed to the same text is deliberately left alone, because
	//     it is an offer to an address rather than a record of a person, the address
	//     became reusable the moment the account was deleted, and blanking it would
	//     break the redemption comparison for whoever takes it next.
	//
	//     Empty string rather than the tombstone, for the reason `users.email` is
	//     one: redemption compares a redeeming account's address against this
	//     column, and a placeholder that reads like a label is a value that
	//     comparison would then have to rule out. `invitations_outstanding_email_key`
	//     cannot collide on the blanks — it excludes redeemed rows, which is every
	//     row this reaches.
	//
	// **The label is a constant and the ids survive** — D148, owner-set 2026-08-08.
	// Nothing is derived from anything, so there is no derivation to reverse.
	// Correlating one erased actor's entries is `audit_logs_actor_idx`, which is
	// keyed on `actor_user_id` and never reads the label. The accepted cost is that a
	// surviving uuid is pseudonymous rather than anonymous data, which
	// `docs/SECURITY.md` states in those words.
	//
	// **Re-entrant, because the two-leader window during a rolling deploy is a
	// stated property of this scheduler** (`cmd/linkctrl/jobs.go:117-127`).
	// `FOR UPDATE SKIP LOCKED` means a second leader takes a disjoint batch instead
	// of waiting for the first, and the guard on each id-matched scrub makes a second
	// pass over the same row a no-op rather than a rewrite. Three of those four
	// compare against the tombstone they write; `invitations` blanks its column
	// instead of labelling it, so its guard is an emptiness test on `i.email` beside
	// `i.redeemed_at IS NOT NULL` — the same claim in the vocabulary that column
	// uses. Running the pass twice and diffing is what the test asserts. The
	// **three** value-matched scrubs are re-entrant for a different reason and it is
	// worth stating: after the first pass the address they matched on no longer
	// exists in the column, so the second pass finds nothing to rewrite.
	//
	// Ordered by `deleted_at`, oldest first, which is the order
	// `users_pending_erasure_idx` stores and the order the requests arrived in.
	//
	// `pending` carries the address as well as the id, because two of the scrubs
	// below have nothing else to match on and the final UPDATE blanks it. That is
	// safe rather than lucky: every CTE in a statement reads one snapshot, so the
	// value here is the pre-erasure one no matter which order the executor runs them
	// in.
	// The account row itself, scrubbed in place. It survives, which is the whole
	// difference between `anonymized_at` and `deleted_at`: foreign keys and audit
	// records go on pointing at a row that identifies nobody.
	//
	// `email` becomes the empty string rather than a placeholder address. No live
	// query can reach it — every read of `users` filters `deleted_at IS NULL` — and
	// the partial unique index excludes the row, so the address it held was already
	// reusable the moment the account was deleted.
	EraseDeletedAccounts(ctx context.Context, arg EraseDeletedAccountsParams) ([]uuid.UUID, error)
	// The verification lookup, on the unique prefix index, joined with the user so
	// authentication is one round trip. Revoked and expired keys are returned
	// rather than filtered out: the caller distinguishes them so the response can
	// say which it was, and a deleted user's key resolves to no row at all.
	//
	// grace_expires_at comes back for the same reason revoked_at does: a rotated
	// predecessor stops verifying when its window closes, and that refusal is
	// decided here rather than by the housekeeping job that later writes revoked_at.
	//
	// owner_is_member is the membership the key leans on, asked here so that
	// authentication stays one round trip. A key acts *as its owner*, and an owner
	// with no membership covering the key's scope has no authority for it to act
	// with — so the credential is invalid rather than merely powerless. The
	// predicate matches GetUserPermissions exactly: an organization-wide membership
	// covers every workspace, a workspace-scoped one covers its own, and an
	// unpinned key is covered only by an organization-wide membership, because
	// NULL = NULL is not true.
	//
	// **Which organization it asks about is no longer the key's column** (M54). A
	// pinned key still asks about the organization it names, and that branch is
	// unchanged. An account-wide one has no column to ask about: it asks whether
	// the owner holds an organization-wide membership in *any* organization this
	// key has not been barred from, which is the same question one step earlier —
	// can this credential reach anywhere at all. Which organization it then lands
	// in is ResolveOrganizationForAPIKey's, and the membership covering that one is
	// guaranteed by the same join, so the precise test and this coarse one cannot
	// disagree.
	//
	// Returned as a column rather than joined into the WHERE clause for the reason
	// revoked and expired keys are returned rather than filtered: the caller decides
	// what each state means, and a refusal a reader can see beside the others is
	// worth more than a row that silently fails to exist.
	GetAPIKeyByPrefix(ctx context.Context, prefix string) (GetAPIKeyByPrefixRow, error)
	// The predecessor, locked, so two rotations of one key serialize instead of
	// racing.
	//
	// FOR UPDATE rather than an optimistic conditional write: the loser of a race
	// should be told "this key has already been rotated" by the check that follows,
	// which is a sentence somebody can act on, not have its transaction rolled back
	// by a unique-index violation on a column it never named.
	//
	// owner_is_member and owner_status come back for the reason revoked_at does:
	// they are re-read here, under the lock, so a membership removed or an account
	// deactivated between authentication and this statement wins. Without them a
	// removal racing a rotation leaves behind a successor the removal was meant to
	// kill, on a chain that can rotate again. A soft-deleted account reads
	// 'deleted' rather than NULL, so the one comparison at the call site covers it
	// and no branch depends on a scan of an absent row.
	GetAPIKeyForRotation(ctx context.Context, id uuid.UUID) (GetAPIKeyForRotationRow, error)
	// The reach of a key named by id, for the administrator's arm of Revoke (M54).
	//
	// Read before the write because the write depends on the answer: a pinned key
	// is revoked outright and an account-wide one has an organization cut out of
	// it, and those are different statements against different tables. Returns the
	// owner too, because whichever it turns out to be, the record has to name whose
	// credential was stopped.
	//
	// No organization predicate, deliberately. Scoping the *read* would make an
	// account-wide key unfindable by every administrator, since it matches none of
	// them; the authorization is at the call site, which refuses unless the owner
	// is a member of the organization the actor holds authority in. A key whose
	// owner has never been in the actor's organization is ErrNotFound, exactly as an
	// unknown id is.
	GetAPIKeyReach(ctx context.Context, id uuid.UUID) (GetAPIKeyReachRow, error)
	GetAutomationRule(ctx context.Context, arg GetAutomationRuleParams) (AutomationRule, error)
	// Reads one entry by its exact host.
	//
	// The decision path's read, and deliberately not MatchBlockedDestination: that
	// one walks a host and every parent of it and answers with the longest match,
	// which is the right question when *judging* a destination and the wrong one
	// when acting on a dispute. The dispute already names the row it is about —
	// destination_disputes.blocked_host, written when it was filed — so the only
	// thing left to ask is whether that row is still there and who owns it (03300).
	//
	// No rows means the entry has gone since the dispute was filed, which the caller
	// reports rather than papering over: an allow that deleted nothing must not be
	// recorded as one that did.
	GetBlockedDestination(ctx context.Context, host string) (GetBlockedDestinationRow, error)
	GetBuiltinRoleBySlug(ctx context.Context, slug string) (GetBuiltinRoleBySlugRow, error)
	// Workspace-scoped like GetLink and GetFolder, and for the same reason: the
	// wrong workspace returns no rows rather than a row the caller must remember to
	// reject.
	GetCampaign(ctx context.Context, arg GetCampaignParams) (Campaign, error)
	// What the dashboard shows beside a gated link. Never on the redirect path.
	GetClickBudget(ctx context.Context, linkID uuid.UUID) (GetClickBudgetRow, error)
	// One domain's settings and where its root points.
	//
	// **The filter its own comment promised** (M40). It read `WHERE is_default`,
	// which was the whole truth while an instance had exactly one domain and became
	// a way of asking the wrong row the moment it had several: a verified custom
	// hostname has a root of its own, and reading its settings through a predicate
	// that can only ever return the default would answer about somebody else's
	// hostname.
	//
	// Not scoped by owner, like every other statement addressed by id in this
	// schema. link.Service has already judged the actor against the row.
	GetDefaultDomainSettings(ctx context.Context, domainID uuid.UUID) (GetDefaultDomainSettingsRow, error)
	// The code an untagged scan resolves through (M50's reopening, D183).
	//
	// One flagged row at most, which `qr_codes_link_default_key` (04400) is what
	// makes true: a partial unique index over `link_id WHERE is_default`. No rows
	// means the link's default code has never been written down — the synthesised
	// default D139 describes — and the service answers for it at the product style
	// rather than reporting an absence.
	//
	// **The empty slug is a fallback rather than the answer, and it is the second
	// half of what makes this migration safe.** 03700's identity was `slug = ”`,
	// and a row can still arrive carrying it and not the flag: written by the
	// previous release during a rolling deploy, when `is_default` is a column it
	// does not know about, or written by hand. Reading the flag alone would report
	// such a link as having no default at all, and the next style write would then
	// insert a second unnamed row against `qr_codes_link_slug_key`. Preferring the
	// flag and falling back to the empty slug costs one ORDER BY and makes both
	// spellings of the same fact resolve to the same row. `LIMIT 1` because the two
	// can name different rows only on a link that has more than one code, where the
	// empty slug does not occur at all.
	GetDefaultQRCode(ctx context.Context, arg GetDefaultQRCodeParams) (GetDefaultQRCodeRow, error)
	GetDestinationDispute(ctx context.Context, id uuid.UUID) (GetDestinationDisputeRow, error)
	// One domain's bot policy, by id.
	//
	// Read on the management path only, and only when a link's own setting is being
	// changed: the service has to know whether the domain enforces before it can
	// tell the caller their `off` will not be honoured. The redirect path never
	// runs this — it gets the same two columns from ResolveAliasForRedirect's join,
	// which is the whole reason that join exists.
	GetDomainBotSettings(ctx context.Context, id uuid.UUID) (GetDomainBotSettingsRow, error)
	// Matches domains_hostname_key exactly — lower(hostname) among the undeleted —
	// so the availability check and the unique index cannot disagree about which
	// names collide.
	GetDomainByHostname(ctx context.Context, lower string) (Domain, error)
	// The row the ownership check is made against, so it carries both owner columns.
	GetDomainByID(ctx context.Context, id uuid.UUID) (Domain, error)
	// Workspace-scoped like GetLink, and for the same reason: the wrong workspace
	// returns no rows rather than a row the caller must remember to reject. This is
	// also the only check that a link is being filed into a folder of its own
	// workspace — `links.folder_id` has a foreign key to `folders(id)` and nothing
	// in it mentions tenancy.
	GetFolder(ctx context.Context, arg GetFolderParams) (Folder, error)
	// Redemption's only lookup, and the row it locks.
	//
	// FOR UPDATE OF i serializes two redemptions of the same token: the second
	// blocks until the first commits and then reads redeemed_at set, so single-use
	// is enforced by the database rather than by a check-then-act in Go. The joined
	// tables are not locked — they are read for their labels, and locking a role
	// row would block every other invite that names it.
	//
	// Expiry and revocation are deliberately NOT in the WHERE clause. The caller
	// has to tell "no such token" from "expired" apart to decide what to log, and
	// it answers all of them identically to the person redeeming (decision D27).
	GetInvitationByTokenHash(ctx context.Context, tokenHash []byte) (GetInvitationByTokenHashRow, error)
	// How long ago each job last succeeded, in seconds.
	//
	// Read from the database rather than kept in the process, and that is the whole
	// point of it. `linkctrl_job_last_success_timestamp_seconds` is set by whichever
	// replica ran the job and resets to absent on restart, so on a multi-replica
	// deployment it answers differently depending on which one Prometheus scraped
	// and it forgets everything a rolling deploy touched. job_state is shared, so
	// every replica reports the same number and a restart does not make a stalled
	// job look healthy.
	//
	// A job that has never succeeded is excluded rather than reported as infinitely
	// stale. Inventing a series for it would make every fresh instance look broken
	// for its first few seconds, and an absent series is what the alert recipe in
	// docs/operations.md is written against.
	GetJobStaleness(ctx context.Context) ([]GetJobStalenessRow, error)
	// --- job bookkeeping ---------------------------------------------------------
	// The point a job is known to have completed through. Rollups recompute rather
	// than accumulate, so this is not a correctness dependency for a run that
	// happens on schedule — it exists for the run that does not. Without it,
	// RunRecent covered a fixed yesterday-and-today window, and any downtime that
	// spanned a UTC day left that day with no rollup and nothing to notice it: the
	// raw events were still there, but nothing ever aggregated them again.
	GetJobWatermark(ctx context.Context, job string) (*time.Time, error)
	// Workspace-scoped by design. Passing the workspace here rather than checking
	// it after the fetch makes cross-tenant reads impossible to write by accident:
	// the wrong workspace returns no rows rather than a row the caller must
	// remember to reject.
	GetLink(ctx context.Context, arg GetLinkParams) (Link, error)
	GetLinkByAlias(ctx context.Context, arg GetLinkByAliasParams) (Link, error)
	GetLinkDimensions(ctx context.Context, arg GetLinkDimensionsParams) ([]GetLinkDimensionsRow, error)
	// The gates a link can put in front of its destination (M35).
	//
	// Three of these run on the redirect path and one does not, and the split is
	// what keeps the gates off the budget of every link that does not use them.
	// Nothing here is consulted for a link whose snapshot says it is ungated; the
	// snapshot carries the flags, the flags decide, and only then does anything
	// below execute.
	// The argon2id hash, read only on the password-submit path.
	//
	// **The cached snapshot never carries this.** It carries a bare boolean, so a
	// Redis dump — or a snapshot payload logged by accident — cannot yield an
	// offline cracking target for every password link on the instance. The price is
	// this query, and it is paid once per submitted password rather than once per
	// visit: a GET that renders the challenge never runs it, and a link with no
	// password never reaches it.
	//
	// Addressed by link id rather than by alias, because the id came out of the
	// snapshot the resolver already produced and re-deriving it from the alias would
	// be a second lookup of a row we have already identified.
	GetLinkPasswordHash(ctx context.Context, id uuid.UUID) (*string, error)
	// The per-QR-code breakdown (M50).
	//
	// **A filter over GetLinkDimensions' rows, not a rollup of its own.** Every
	// value here was written by RollupDimensionDaily's ordinary `referrer` pass,
	// because a scan's code is stored *as* its referrer value — `qr:<slug>` for a
	// scan that named a code, and the bare `qr` for one that named none, which the
	// reader counts against whichever code is the default (D183). So this milestone
	// added no pass over click_events, no column and no dimension name: the thing
	// that made a per-campaign rollup too expensive to include in this phase is the
	// thing this does not do.
	//
	// It is a separate statement rather than a reuse of GetLinkDimensions because
	// that one is bounded at twenty rows ordered by clicks, and a link whose busiest
	// referrers are twenty real hostnames would lose its own codes off the end of
	// its own breakdown. Same table, same index, same shape — only the predicate and
	// the bound differ, and the bound is domain.MaxQRCodesPerLink + 1 because the
	// default code is one more than the cap counts.
	//
	// `value = 'qr' OR value LIKE 'qr:%'` cannot collide with a real referrer. The
	// column otherwise holds hostnames and the `direct` sentinel, and a colon is not
	// a character a hostname may contain.
	GetLinkQRDimensions(ctx context.Context, arg GetLinkQRDimensionsParams) ([]GetLinkQRDimensionsRow, error)
	// Reads the rollup, never the raw events. This is what keeps analytics under
	// the 2s target as click_events grows into the tens of millions.
	GetLinkStats(ctx context.Context, arg GetLinkStatsParams) ([]GetLinkStatsRow, error)
	GetLinkTags(ctx context.Context, linkID uuid.UUID) ([]GetLinkTagsRow, error)
	// One membership, scoped by organization so an id from elsewhere is
	// indistinguishable from one that never existed.
	//
	// FOR UPDATE OF m: every caller is about to re-role or delete this row, and the
	// rank check that decides whether they may is read from it. Without the lock,
	// two administrators acting at once could each read a state the other is
	// changing — the check-then-act that the last-owner refusal exists to prevent.
	GetMembership(ctx context.Context, arg GetMembershipParams) (GetMembershipRow, error)
	//
	// One row of the actor's own inbox, scoped by user_id like every statement
	// around it: somebody else's notification is "no rows" rather than a 403 that
	// confirms the id exists.
	//
	// Read by the click-through (M48). Where a notification leads is computed from
	// its `kind` and its `data`, and both have to come off the row — a destination
	// carried on the request would be a redirect target the caller chose.
	GetNotification(ctx context.Context, arg GetNotificationParams) (Notification, error)
	// The organization being deleted, read after LockOrganizations has locked it.
	//
	// Its name and slug are read here because the audit record has to carry them:
	// once the row is gone that record is the only remaining trace of what was
	// deleted, exactly as it is for a workspace.
	GetOrganization(ctx context.Context, id uuid.UUID) (Organization, error)
	// Whether a user is in an organization at all, for the grant path:
	// workspace-scoped access is given to somebody who is already a member, and
	// this is what establishes that.
	//
	// **Any** membership counts, organization-wide or workspace-scoped. Requiring
	// an organization-wide one would be a dead end: somebody left holding only a
	// workspace-scoped membership could never be given a second workspace, because
	// re-inviting them is refused as already-a-member. Under D31 every grant adds,
	// so widening a scoped member to a second workspace is the same kind of act as
	// the first grant was.
	//
	// The organization-wide row wins the tiebreak so the label and role this
	// returns are the person's broadest, which is what a control naming them should
	// show.
	GetOrganizationMember(ctx context.Context, arg GetOrganizationMemberParams) (GetOrganizationMemberRow, error)
	// What to call the organization in an invitation. A primary-key lookup on a
	// path that is already writing a row, rather than carrying a name on every
	// identity for the one surface that needs it.
	GetOrganizationName(ctx context.Context, id uuid.UUID) (string, error)
	// The reset lookup, inside the transaction that spends the row.
	//
	// FOR UPDATE, so two submissions of the same link serialize and the second sees
	// the row the first consumed. The join is what makes the account's own state
	// reachable in one round trip: `status` and `password_hash` are both refusals
	// this path has to make, and reading them separately would leave a gap between
	// the check and the write.
	GetPasswordResetByTokenHash(ctx context.Context, tokenHash []byte) (GetPasswordResetByTokenHashRow, error)
	// Verification's lookup, inside the transaction that spends the row.
	//
	// FOR UPDATE, so two clicks on the same link serialize and the second sees the
	// row the first consumed.
	GetPendingRegistrationByTokenHash(ctx context.Context, tokenHash []byte) (PendingRegistration, error)
	// **`q.*` is gone from the four reads below, and that is M50.5 rather than
	// style.** *(Three when M50.5 wrote this; `GetDefaultQRCode` is D183's, and it
	// carries the same explicit list for the same reason.)* `qr_codes` now carries a `logo bytea` (03800, D134) bounded at
	// qr.MaxLogoStoredBytes — a little over a megabyte a row — and a link may hold
	// domain.MaxQRCodesPerLink of them. A star projection would fetch every one of
	// those bytes to draw a list of names, so the reads carry an explicit column
	// list and report the logo as the one fact a reader of the list needs:
	// **whether there is one**. `logo IS NOT NULL` is answered from the row's TOAST
	// pointer without detoasting the value, so asking costs nothing.
	//
	// The bytes themselves are read by nothing here. Nothing in M50.5 serves a
	// stored logo back — the two operations are set and clear — and M50.6, which
	// composites one into a picture, is where a query that reads them belongs.
	// One code of a link's, by slug. No rows means the code does not exist, and the
	// service reports that.
	//
	// **The default code is not reachable here and that is the point** (D183). It
	// used to be `slug = ”`; it is now whichever row carries `is_default`, which is
	// GetDefaultQRCode's job, because a caller that wanted "the default" and passed
	// the empty string would silently match nothing at all now that no row holds it.
	GetQRCode(ctx context.Context, arg GetQRCodeParams) (GetQRCodeRow, error)
	//
	// **The only read in this file that projects the column**, and it is separate
	// from GetQRCode rather than folded into it for exactly the reason the three
	// reads above stopped saying `q.*`: drawing a list of twenty names must not
	// fetch twenty images. This is called once, by a surface that is about to
	// composite one code's logo into one picture, and only for a code whose
	// `has_logo` already said there is something to fetch.
	//
	// NULL comes back for a code with no logo, which the service reads as "nothing
	// to draw" rather than as an error: `has_logo` and this can disagree by exactly
	// one concurrent clear, and the honest answer to that race is the picture
	// without the logo.
	GetQRCodeLogo(ctx context.Context, arg GetQRCodeLogoParams) ([]byte, error)
	// The live-activity feed. Bounded and index-backed on (link_id, occurred_at).
	GetRecentClicks(ctx context.Context, arg GetRecentClicksParams) ([]GetRecentClicksRow, error)
	GetRoleBySlug(ctx context.Context, slug string) (Role, error)
	GetRoutingRule(ctx context.Context, arg GetRoutingRuleParams) (GetRoutingRuleRow, error)
	// Analytics: salts, rollups and reads.
	GetSalt(ctx context.Context, validOn time.Time) ([]byte, error)
	// Joined with the user so validating a session is one round trip on a path
	// that runs for every authenticated request. Filters revoked and deleted here
	// rather than in Go, so a revoked session cannot be resurrected by a caller
	// that forgets to check.
	GetSessionByTokenHash(ctx context.Context, tokenHash []byte) (GetSessionByTokenHashRow, error)
	GetTagByName(ctx context.Context, arg GetTagByNameParams) (Tag, error)
	// Comparison is on the generated email_lower column, so callers cannot
	// accidentally do a case-sensitive lookup and create a duplicate account.
	GetUserByEmail(ctx context.Context, email string) (User, error)
	GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
	// A second factor (M53): TOTP, enrolment, recovery codes, and the step between
	// a right password and a session.
	//
	// Three groups of statements. The enrolment pair writes `users.mfa_secret` and
	// `users.mfa_enabled_at` — the columns `00200_identity.sql` has carried since the
	// first migration and only M52's erasure sweep has ever touched. The recovery-code
	// statements are a hashed single-use credential, the fifth thing in this schema
	// shaped that way. And the pending-login statements are the state machine that
	// makes "no session token exists until the second factor is verified" a property
	// of the database rather than of a handler's control flow.
	// Everything the second factor needs about one account, and nothing else.
	//
	// Not `SELECT *`: the enrolment and challenge paths have no business holding a
	// password hash, and a narrow row is what keeps that true as columns are added.
	GetUserMFA(ctx context.Context, id uuid.UUID) (GetUserMFARow, error)
	// The RBAC evaluator's source of truth. Returns every permission a user holds
	// in a workspace, via their organization membership and its role.
	//
	// A NULL memberships.workspace_id means the membership covers every workspace
	// in the organization, which is what Phase 1 always creates.
	GetUserPermissions(ctx context.Context, arg GetUserPermissionsParams) ([]string, error)
	GetUserRoleInWorkspace(ctx context.Context, arg GetUserRoleInWorkspaceParams) (GetUserRoleInWorkspaceRow, error)
	GetVariantRule(ctx context.Context, arg GetVariantRuleParams) (GetVariantRuleRow, error)
	GetWebhook(ctx context.Context, arg GetWebhookParams) (GetWebhookRow, error)
	// The hostname a new link goes on when the caller names none.
	//
	// **This is the filter the name promised and the query never had** (M40). It
	// read `WHERE is_default` with no workspace argument at all, so every workspace
	// on the instance got the same answer and the word "workspace" in the name was
	// describing an intention rather than a predicate.
	//
	// A workspace's own *verified* hostname wins over the instance default, which is
	// what registering one is for; the instance default is the fallback and is what
	// every workspace without one still gets, unchanged. Organization-owned
	// hostnames sit between the two — every workspace in the organization may use
	// one, so it is more specific than the instance and less than a workspace's own.
	//
	// **Verified only.** An unverified hostname is not a routing target, so putting
	// a link on it would mint a short URL that resolves nowhere; the ordering below
	// cannot reach one because the WHERE clause has already excluded it.
	//
	// Ties are broken by verified_at then id, so the answer is stable: a workspace
	// that verifies a second hostname does not silently move its new links onto it.
	// organization_id and workspace_id are selected because they are the domain's
	// *scope*, and the scope is what decides whether an alias collision on it could
	// involve a workspace the caller cannot see. A refusal that cannot tell a shared
	// namespace from a private one has to be worded for the worst case or say
	// nothing useful at all (F23).
	GetWorkspaceDefaultDomain(ctx context.Context, arg GetWorkspaceDefaultDomainParams) (GetWorkspaceDefaultDomainRow, error)
	// One workspace, scoped by organization so an id belonging to another tenant is
	// indistinguishable from one that does not exist.
	//
	// FOR UPDATE: both callers — rename and delete — are about to write this row or
	// the rows that cascade from it, and delete reads a link count that must not
	// change underneath the decision.
	GetWorkspaceInOrganization(ctx context.Context, arg GetWorkspaceInOrganizationParams) (Workspace, error)
	// The HMAC key for one workspace, read on the redirect path only for links whose
	// snapshot says they require a signature — and cached in process by the caller,
	// so a signed link costs one query per workspace per process rather than one per
	// request. NULL means the workspace has never minted one, which means nothing
	// in it can carry a valid signature.
	GetWorkspaceSigningSecret(ctx context.Context, id uuid.UUID) ([]byte, error)
	GetWorkspaceStats(ctx context.Context, arg GetWorkspaceStatsParams) ([]GetWorkspaceStatsRow, error)
	// Summing daily uniques over-counts anyone visiting on more than one day.
	// Reported as "unique visitors per day, summed" in the UI rather than
	// presented as a distinct-person count, because the exact figure cannot be
	// recovered once the salts are purged. That is the intended trade.
	GetWorkspaceTotals(ctx context.Context, arg GetWorkspaceTotalsParams) (GetWorkspaceTotalsRow, error)
	// Confer one instance-level permission on one account.
	//
	// Idempotent. Re-conferring what somebody already holds is the ordinary result
	// of two administrators doing the same obvious thing, and it must not turn into
	// an error that reads like a refusal; the original granted_by and granted_at
	// stand, because the first grant is the one that happened.
	//
	// It returns the row count for a reason that has nothing to do with idempotence:
	// the SELECT finds no row for a slug that does not exist, so a typo would confer
	// nothing and report success. On the setup path that means an instance that has
	// been claimed and has nobody who can administer it, which is the one outcome
	// this whole table exists to prevent. The caller distinguishes 0 from 1 by
	// reading the count against a permission it already knows the account did not
	// hold; see internal/auth's grantInstancePrincipal.
	GrantInstancePermission(ctx context.Context, arg GrantInstancePermissionParams) (int64, error)
	// Whether the instance owner has allowed this host (M32).
	//
	// Read at exactly one call site — internal/link's feed step — and that
	// confinement is the whole safety argument. It suppresses the third-party
	// reputation feed for a host the owner already decided about, which is what
	// makes a feed verdict owner-overridable without 01500 growing the allow column
	// it deliberately does not have.
	//
	// It cannot widen anything else. The three tiers above the feed have all
	// returned by the time this runs, and M31 refuses to file a dispute about any
	// refusal but a low-confidence one, so no row here can carry an unappealable or
	// embedded-tier reason code to be read as permission.
	//
	// Equality rather than the blocklist's candidate walk: allowing 'evil.example'
	// says nothing about 'login.evil.example', and 01700's partial index matches
	// this predicate exactly.
	HostHasAllowedDispute(ctx context.Context, host string) (bool, error)
	// Audit log. Append-only: there is no update and no delete here, and that is the
	// point of the table. Rows leave only when retention drops a whole partition.
	InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error
	// Blocked-attempt disputes (M31).
	//
	// Read and written on the management path only. Like the blocklist it argues
	// with, nothing here is reachable from the redirect tree: a dispute is about
	// what may be *stored*, and a link that was refused at creation never became a
	// row for a visitor to resolve.
	// Files one dispute.
	//
	// Two unique partial indexes decide whether this is a duplicate, and both do it
	// in the database rather than in a check-then-insert two requests can both pass.
	// 01600's is on (host) WHERE status = 'open' — one open dispute per host as
	// typed. 03300's is on (blocked_host) WHERE status = 'open' AND blocked_host
	// <> ” — one open dispute per *blocklist row*, so a caller cannot put the same
	// decision in front of the owner once per subdomain of it.
	//
	// @blocked_host is the row the refusal matched, which is routinely a parent of
	// @host. Empty when the rule is computed from the URL rather than held on the
	// list, and the second index skips those: every one of them would carry the same
	// key, and one open homograph dispute must not lock out every other.
	InsertDestinationDispute(ctx context.Context, arg InsertDestinationDisputeParams) (DestinationDispute, error)
	// One code of a set. Called ten times inside the transaction that issues them,
	// rather than as one multi-row insert, because the hashes are computed one at a
	// time and a loop over a prepared statement is what sqlc gives without a bespoke
	// array parameter for a fixed ten rows.
	InsertMFARecoveryCode(ctx context.Context, arg InsertMFARecoveryCodeParams) error
	// Notifications. The table shipped dormant in 00600; nothing here adds a
	// column, per the rule that a dormant table's structure goes in its jsonb until
	// the feature that needs a column arrives. `data` carries whatever a kind needs.
	InsertNotification(ctx context.Context, arg InsertNotificationParams) error
	// Consulted by BOTH create paths — generated aliases before insert, and
	// user-supplied aliases as validation — and by alias changes.
	//
	// No deleted_at filter on the links branch, deliberately: a soft-deleted row
	// holds its alias for the whole trash window, so a link deleted by accident can
	// be restored under its own name. The partial unique index cannot enforce that
	// (it ignores trashed rows), so this check is the enforcement and the index
	// remains the guarantee against live-row races only.
	IsAliasTaken(ctx context.Context, arg IsAliasTakenParams) (bool, error)
	// Which organizations have been cut out of which of these keys (F178).
	//
	// The other half of the reach revocation M54 built. An administrator can bar an
	// account-wide key from their organization and the key's owner had no way to
	// learn it: the list above reports one reach — pinned or account-wide — and a
	// credential that had silently stopped resolving into one tenant read exactly as
	// it did the day before. The audit record that says why is written in the
	// administrator's organization, which the owner may hold no `audit.read` in.
	//
	// Keyed on the ids already listed rather than on the owner, so it is one round
	// trip after the list and not one per key, and so it cannot return a bar on a
	// key the caller was not shown.
	//
	// The organization's name is joined because an id answers nobody's support
	// question, and it discloses nothing new: a bar only ever exists on a key whose
	// owner held an organization-wide membership there when it was written.
	//
	// Soft-deleted organizations are not filtered out. The bar is a fact about the
	// key, it outlives the tenant, and hiding the row would make a key look
	// unrestricted when it is not.
	ListAPIKeyOrgRevocations(ctx context.Context, apiKeyIds []uuid.UUID) ([]ListAPIKeyOrgRevocationsRow, error)
	// Revoked keys are included. "Which keys existed and when were they revoked"
	// is the question asked after an incident, so they are listed until the reaper
	// removes them.
	//
	// workspace_id is selected because NULL is a state the owner chose (M44): a key
	// bound to one workspace and a key valid across the organization look identical
	// without it. organization_id is selected for the same reason one tier up
	// (M54): NULL there is account-wide and non-NULL is pinned, and a list that
	// omitted it would show two credentials with different reach as one row shape.
	//
	// **Owner alone, no organization predicate** (M54, closing F75). It used to
	// carry `AND organization_id = $2` while RevokeAPIKey carried only the owner,
	// and the two statements disagreeing about which keys an actor reaches was the
	// whole of that finding: a key listed nowhere still revoked, so 204-versus-404
	// answered "is this id one of mine, elsewhere". They agree now because the
	// question they both ask is the same one — whose key is this — and an
	// account-wide key has no organization to filter on in the first place.
	ListAPIKeysForUser(ctx context.Context, userID uuid.UUID) ([]ListAPIKeysForUserRow, error)
	//
	// Newest first, keyed on (occurred_at, id) so the cursor is a position rather
	// than an offset: an event written while a reader is paginating shifts every
	// offset by one, and a keyset cursor is unaffected by it.
	//
	// The row-comparison predicate is what makes that hold. Comparing the columns
	// separately -- occurred_at < c OR (occurred_at = c AND id < i) -- is the same
	// logic, and the planner does not always recognise it as a range scan on the
	// (organization_id, occurred_at DESC) index.
	//
	// Scoped by organization, never by the workspace the reader happens to be
	// *acting in*: an audit log that narrowed itself to the current workspace would
	// hide exactly the actions worth reviewing. That is M21's argument and it still
	// holds; what it never said is that the reader's own authority does not bound
	// the rows either.
	//
	// It does now (F31). `org_wide` is true when the reader holds audit.read from an
	// organization-wide membership, which is the only membership that reaches the
	// organization-wide scope (auth.MembershipAuthority, D44) — such a reader sees
	// every row, exactly as before. A reader whose audit.read comes only from
	// workspace-scoped memberships sees the rows of those workspaces and nothing
	// else, because a workspace-scoped membership grants authority over its own
	// workspace and not over the organization.
	//
	// Rows with a NULL workspace_id are organization-level acts, and `= ANY` is
	// false against NULL, so a workspace-scoped reader does not see them. That is
	// the same asymmetry MembershipAuthority.In(nil) enforces for writes, arriving
	// here for reads.
	ListAuditLogs(ctx context.Context, arg ListAuditLogsParams) ([]AuditLog, error)
	ListAutomationRules(ctx context.Context, workspaceID uuid.UUID) ([]AutomationRule, error)
	// The four seeded roles, most powerful first. Feeds the invite form's role
	// choices, filtered by the inviter's own rank in the service (decision D28).
	ListBuiltinRoles(ctx context.Context) ([]ListBuiltinRolesRow, error)
	// Every campaign in the workspace, with how many live links carry it.
	//
	// The count is a grouped scan of links_campaign_idx rather than a correlated
	// subquery per campaign, exactly as ListFolders counts filed links: a workspace
	// with many campaigns costs one pass instead of one index probe each.
	//
	// Unpaginated, and bounded instead by domain.MaxCampaignsPerWorkspace. A
	// campaign list is a picker as much as it is a page — the link form offers it —
	// and a picker that paginates is a picker nobody can choose from.
	ListCampaigns(ctx context.Context, workspaceID uuid.UUID) ([]ListCampaignsRow, error)
	// The queue, newest first, keyset on (created_at, id).
	//
	// Instance-wide rather than scoped to the reader's organization, because the
	// list a decision acts on is instance-wide (01500) and a queue narrower than the
	// authority it exercises would hide rows the reader is nonetheless deciding for.
	// The permission is what bounds who sees it.
	//
	// @open_only lets the page show the work and the archive from one query, which
	// is the same shape ListNotifications' unread filter has.
	//
	// The LEFT JOIN carries the blocklist entry's **source**, which is what decides
	// whether an allow can do anything (F42). `liftableRules` says the *rule* is
	// list-backed; it does not say the entry behind this particular refusal is one
	// a decision may delete. An `env`-sourced entry comes from
	// LINKCTRL_DESTINATION_BLOCKLIST and is rewritten at every boot, so removing it
	// would be undone by the next restart and `entryToLift` refuses — while the page
	// drew the Allow button from the rule alone and the operator found out by
	// clicking. LEFT, because a refusal computed from the URL has no entry at all
	// and must stay in the queue.
	ListDestinationDisputes(ctx context.Context, arg ListDestinationDisputesParams) ([]ListDestinationDisputesRow, error)
	// Every domain the caller may use: the instance default, whatever their
	// organization owns, and whatever their own workspace owns.
	//
	// Another workspace's hostname is absent rather than present and unmanageable.
	// A list that showed it would disclose which hostnames a neighbouring workspace
	// has registered, and the registration is the only thing there is to disclose at
	// this milestone.
	//
	// Ordered default-first, then by hostname: the default is the one every link is
	// on today, so it belongs at the top rather than wherever its placeholder name
	// happens to sort.
	ListDomains(ctx context.Context, arg ListDomainsParams) ([]ListDomainsRow, error)
	// --- evaluation --------------------------------------------------------------
	//
	// The rules one run considers, least recently looked at first.
	//
	// Bounded by the caller at domain.AutomationRulesPerRun, and ordered so the cap
	// starves nobody — which is a claim this statement can now make, because
	// `last_checked_at` moves for **every** rule a run reached and not only for the
	// ones that fired. The rules a capped run skipped keep the older cursor and go
	// first next time. A cap without this order would evaluate whichever workspace
	// happened to sort first, forever.
	//
	// **Ordering on `last_fired_at` is what F83 was**, and the difference is not
	// cosmetic: that column moves only on a firing, idle is exactly what keeps it
	// old, and so the hundred oldest were a fixed set and rule 101 was never
	// evaluated on any run. The two columns are separate facts and 03100 separates
	// them.
	//
	// The organization is joined in here rather than fetched per rule, because every
	// rule that fires with a `notify` action needs it and N+1 lookups to assemble a
	// batch is the wrong shape. Walks automation_rules_due_idx, as rebuilt by 03100.
	ListDueAutomationRules(ctx context.Context, rowLimit int32) ([]ListDueAutomationRulesRow, error)
	// Every folder in the workspace, with how many links are filed directly in it.
	//
	// Flat and unordered by structure: the caller builds the tree. Sorted by name so
	// that the assembled tree's sibling order is stable, and by id after it so two
	// names that fold to the same string under a collation cannot swap between page
	// loads.
	//
	// The count is a grouped scan of links_folder_idx rather than a correlated
	// subquery per folder, so a workspace with many folders costs one pass instead
	// of one index probe each. It counts links filed *directly* here — a parent does
	// not report its children's links, because the number beside a folder has to
	// mean the same thing as the number of rows the list shows when you click it.
	ListFolders(ctx context.Context, workspaceID uuid.UUID) ([]ListFoldersRow, error)
	//
	// The instance-wide audit surface (F36, D98). Rows with no organization at all:
	// an act that changed every tenant and belongs to none of them.
	//
	// A separate statement rather than a predicate bolted onto the one above, for
	// two reasons that point the same way. The query above rides
	// audit_logs_org_time_idx as a range scan; an OR reaching NULL organizations
	// would turn it into a bitmap scan and a sort on a table designed to grow
	// forever. And the surface is genuinely separate: it is read by the instance
	// principal under audit.read.instance, not by whoever happens to hold audit.read
	// in some organization, so merging the two would mean deciding per row which
	// permission had authorized it.
	//
	// Same keyset shape, so a client that paginates the organization log paginates
	// this one.
	ListInstanceAuditLogs(ctx context.Context, arg ListInstanceAuditLogsParams) ([]AuditLog, error)
	// Who holds one instance-level permission, with enough of the account to name
	// them on the page that confers it.
	//
	// Soft-deleted accounts are filtered rather than shown as inert: a grant to an
	// account that cannot authenticate is not reach, and listing it invites somebody
	// to revoke a row that was already doing nothing.
	ListInstanceGrantHolders(ctx context.Context, permission string) ([]ListInstanceGrantHoldersRow, error)
	// Instance-level grants: what a person may do to the instance rather than to a
	// tenant (D98). Every statement here joins `permissions` on the slug instead of
	// taking a permission id, so the slugs stay the vocabulary the Go code speaks
	// and no caller has to carry a uuid literal around.
	// Every instance-level permission one person holds.
	//
	// Read on every identity resolution, beside GetUserPermissions, which is why it
	// is keyed on the user alone and returns slugs: the caller folds it into the
	// same set and Identity.Can cannot tell the two sources apart. It deliberately
	// does not join workspaces or memberships — an instance grant is not reached
	// through a tenancy, and an account that belongs to no organization at all (D36)
	// keeps whatever it holds here.
	ListInstanceGrants(ctx context.Context, userID uuid.UUID) ([]string, error)
	// The administrator's list, newest first.
	//
	// No pagination and no cursor. An organization's outstanding invitations are a
	// handful of rows by construction, and redeemed ones stop accumulating the
	// moment people join; the link list's machinery here would be a page that
	// cannot fill.
	//
	// The inviter is joined as a label rather than an id, for the same reason the
	// audit log stores one: the row has to stay readable after that account is
	// gone, and the LEFT JOIN is what lets it.
	ListInvitations(ctx context.Context, organizationID uuid.UUID) ([]ListInvitationsRow, error)
	// Every destination a link has, for the per-destination breakdown to name.
	//
	// Includes the link's own destination at position 0, because a click recorded
	// before the link had a split — or on a link whose split was later removed —
	// carries a NULL destination_id and is attributed to exactly that row. A
	// breakdown that could not name it would be a chart with an unlabelled bar
	// holding most of the traffic.
	ListLinkDestinations(ctx context.Context, arg ListLinkDestinationsParams) ([]ListLinkDestinationsRow, error)
	// Keyset pagination over (created_at, id).
	//
	// The cursor is a composite so ordering is total: created_at alone is not
	// unique, and a tie at the page boundary would drop or duplicate rows.
	// Comparing the pair with row-value syntax lets the composite index serve it
	// directly.
	//
	// Sorting is a CASE rather than three separate queries because sqlc has no
	// dynamic SQL. If plan stability becomes a problem this splits into
	// ListLinksNewest/Oldest/Clicks; measure before doing that.
	// The two tag aggregates are paired positionally by the caller, so they must
	// agree on their order — and on the table they read. Aggregating names from a
	// join and ids from link_tags alone, each sorted by its own column, produced
	// arrays in different orders whenever a link's tags sorted differently by name
	// than by id, and every tag came back carrying another tag's name. One
	// subquery, one ORDER BY, both columns.
	ListLinks(ctx context.Context, arg ListLinksParams) ([]ListLinksRow, error)
	// Membership management: who is in an organization, at what rank, and where
	// that rank reaches (M28).
	//
	// The rows these statements read and write are the ones 00200 has carried since
	// Phase 1. Nothing here is new schema; what is new is that a person can change
	// them, which is why every write is scoped by organization_id as well as by id.
	// Every membership in an organization, most powerful first.
	//
	// One row per membership, not per user. A user holding an organization-wide
	// membership and a workspace-scoped one appears twice, and that is the shape the
	// page has to show: under D31 the two rows *add*, and collapsing them into one
	// would hide the second grant behind the first.
	//
	// The workspace name is joined rather than looked up per row, and is NULL for an
	// organization-wide membership — which is what the absence of a workspace_id
	// means, and the distinction the list is read for.
	//
	// Not paginated. An organization's membership is a handful of rows by
	// construction, exactly as its invitations are.
	ListMembers(ctx context.Context, organizationID uuid.UUID) ([]ListMembersRow, error)
	// Every membership one user holds in an organization, with the rank it carries
	// and whether the role behind it grants a named permission.
	//
	// This is the authorization side of the sentence LockOrganizationOwners states
	// just above: **a workspace-scoped membership grants authority over its own
	// workspace, not over the organization.** The evaluator answers a different
	// question — what may this person do in the workspace they are *acting in* —
	// by taking the union of every matching membership and the lowest rank among
	// them (D31), which is right for an object that lives in a workspace and wrong
	// for one that spans the organization. A workspace-scoped admin resolved into
	// their own workspace otherwise carries rank 20 against an organization-wide
	// membership their membership does not reach at all, and F27 walked exactly
	// that: one dropdown on /members turned them into an organization-wide admin.
	//
	// So the rows come back unfolded, one per membership, and the caller keeps the
	// ones that reach the scope of the object being written. A membership scoped to
	// a deleted workspace reaches nothing, matching GetUserPermissions.
	//
	// The permission is a parameter rather than a join in Go because the answer is
	// per role, not per membership: two memberships at the same role give the same
	// answer, and asking the database means the grant is read from
	// role_permissions — the same table the evaluator reads — rather than from a
	// second list of which roles hold what.
	ListMembershipAuthority(ctx context.Context, arg ListMembershipAuthorityParams) ([]ListMembershipAuthorityRow, error)
	//
	// Newest first, keyset on (created_at, id). Same shape as the audit log and the
	// link list: an offset shifts under a notification arriving mid-page, and a new
	// notification arriving is the normal case here rather than the rare one.
	ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]Notification, error)
	//
	// Every organization on the instance. Phase 1 has exactly one; this is written
	// as a list because M28 makes that untrue and a job that notified only the
	// first organization would then be silently wrong for every other.
	ListOrganizationIDs(ctx context.Context) ([]uuid.UUID, error)
	// Registered hostnames that are not being served, oldest check first.
	//
	// Included deliberately, and second. Somebody who registers a hostname and
	// publishes the record should not have to come back and press a button — the
	// on-demand check exists for the person who does not want to wait, not because
	// waiting is the only other option. What they may now have to wait for is a pass
	// with room left after the serving class, which is the price of the serving class
	// never waiting for them.
	ListPendingDomainsForVerification(ctx context.Context, rowLimit int32) ([]ListPendingDomainsForVerificationRow, error)
	// The scope vocabulary. Scopes are validated against the permissions table
	// rather than a list in Go, so RBAC and API keys cannot drift apart.
	ListPermissionSlugs(ctx context.Context) ([]string, error)
	// Every code a link carries (M50), in alphabetical order by name.
	//
	// Unpaginated, and bounded instead by domain.MaxQRCodesPerLink, which is the
	// same trade ListCampaigns makes: the cap is small enough that a page of them is
	// the whole set, and a pager over a list that cannot exceed it would be a
	// control nobody ever operates.
	//
	// **The default no longer leads, and that reverses what this comment argued**
	// (M50.8). The key was `NOT (is_default OR slug = ”)` — false before true, so
	// the flag-holder led whatever order the rest were created in — and the defence
	// written here was that *"the list re-orders when the reader moves it, which is
	// the visible half of what setting a default does"*. The owner reported the
	// other half of that: *"Selecting a different default code re-orders the list of
	// codes which can make it seem like the selection didn't change."* The argument
	// is answered rather than withdrawn. M50.7 put a filled icon on the row that
	// holds the flag, so being visible is now the icon's job; the sort was carrying
	// it as a side effect, and carrying it was what made the change look like
	// nothing had happened.
	//
	// `lower(q.label)` with `q.id` behind it, which is exactly `ORDER BY
	// lower(c.name), c.id` at ListCampaigns above: one collation for one product,
	// and a tie-break so two codes sharing a name have a stable order rather than
	// whatever the plan returns. An unnamed code sorts first, which is where the
	// default of a link nobody has named codes on already was.
	//
	// **Nothing may read position 0 as the default any more.** link.ListQRCodes and
	// analytics.qrCodeSplit both did — `!rows[0].IsDefault && rows[0].Slug != ""`
	// as the test for "no row holds the flag" — and both scan every row since this
	// milestone. The flag is a column; it was never a position.
	ListQRCodes(ctx context.Context, arg ListQRCodesParams) ([]ListQRCodesRow, error)
	// The management list: every rule on a link, enabled or not, in the order the
	// redirect path would evaluate them.
	//
	// Ordered by (priority, created_at) rather than by priority alone. Priority is
	// not unique, and two rules that tie have to be evaluated in a defined order or
	// the same request resolves differently on two replicas. Creation order is the
	// tiebreak because it is the only one a person can predict from the list they
	// are looking at.
	//
	// Workspace-scoped in the WHERE like every other management read, so a rule
	// belonging to another tenant returns no rows rather than a row the caller has
	// to remember to reject.
	ListRoutingRules(ctx context.Context, arg ListRoutingRulesParams) ([]ListRoutingRulesRow, error)
	// The re-verification job's work list, in two classes (M40, reopened).
	//
	// **One queue could be starved, and the thing it starved was the hard stop.**
	// The job walked a single list ordered `verification_checked_at NULLS FIRST`,
	// which is the right order for one class and fatal across both: `RenameDomain`
	// writes that column back to NULL, so a workspace renaming its rows in a loop
	// kept them at the head of the queue for ever, while a *serving* hostname — which
	// always carries a watermark, because a check is what made it serve — sorted last
	// and was never reached. The only mechanism that takes a lapsed or hijacked
	// hostname out of service therefore stopped running instance-wide, while every
	// pass logged healthy counts.
	//
	// Splitting on `verified_at` closes it exactly, because a rename un-verifies:
	// churn can only ever crowd the *pending* class, and the class whose checks can
	// stop serving is drawn separately and walked first. Neither statement reads a
	// NULL `verification_checked_at` as anything but "not checked yet" — it is what a
	// live renamed row carries, and treating it as abandonment would delete the
	// registration of anybody mid-cut-over.
	// Hostnames this instance is serving, oldest check first.
	//
	// Walked first and given the whole budget it needs, because these are the rows
	// where a failing check has a consequence: the grace window runs against them and
	// `UnverifyDomain` ends it. A pass that runs out of time must run out of it on
	// the class where the cost of waiting is another hour of not-yet-serving, not on
	// the class where it is another hour of serving a name whose DNS is gone.
	ListServingDomainsForVerification(ctx context.Context, rowLimit int32) ([]ListServingDomainsForVerificationRow, error)
	// Counts l.id, not lt.link_id. The join onto links is what excludes trashed
	// links, but counting the link_tags column ignored it: a LEFT JOIN keeps the
	// link_tags row when its link is soft-deleted, so the count included trashed
	// links for the whole 30-day window and the tag list disagreed with the link
	// list it filters.
	ListTags(ctx context.Context, workspaceID uuid.UUID) ([]ListTagsRow, error)
	//
	// The whole of the header's notification lookup: the newest unread rows the bell
	// previews, and the unread total the badge shows, in one round trip.
	//
	// Two shapes already in this file, composed rather than a third one. The
	// predicate is CountUnreadNotifications' predicate character for character, so
	// notifications_user_unread_idx still serves it; the ordering is
	// ListNotifications' ordering, so the preview is the same "newest first" the
	// page shows.
	//
	// `count(*) OVER ()` is what makes it one query instead of two. Window functions
	// are evaluated before LIMIT, so the count is every unread row rather than the
	// handful returned — which is the only reason the badge can keep being exact
	// while the preview stays bounded. A page render costs one notification query
	// here, as it did when it cost a bare count.
	ListUnreadNotificationPreview(ctx context.Context, arg ListUnreadNotificationPreviewParams) ([]ListUnreadNotificationPreviewRow, error)
	ListUserSessions(ctx context.Context, userID uuid.UUID) ([]ListUserSessionsRow, error)
	ListUsers(ctx context.Context) ([]ListUsersRow, error)
	//
	// Who to tell about something that concerns the organization rather than a
	// person. Active users only: a deactivated account cannot sign in to read it.
	//
	// **Scoped, because a membership is** (D44). The sentence LockOrganizationOwners
	// states in members.sql applies here word for word: a workspace-scoped owner
	// membership grants ownership of one workspace, not of the organization. So the
	// recipients are the organization-wide rows, plus — when the news belongs to a
	// workspace — the rows scoped to *that* workspace, because news about their
	// workspace is theirs to hear.
	//
	// Two arms rather than one predicate, and that is the whole correction. Adding
	// `m.workspace_id IS NULL` alone is the smaller diff and the wrong recipient
	// set: it silences a workspace-scoped owner about their own workspace, which is
	// exactly what a caller passing a workspace id means to tell them about. A NULL
	// @workspace_id is news that belongs to no workspace, and the second arm
	// matches nothing then, which is what the organization-wide callers want.
	//
	// r.organization_id IS NULL for the reason LockOrganizationOwners carries it:
	// 'owner' names a built-in role, and a tenant's custom role of the same slug is
	// a different role.
	//
	// DISTINCT because the arms overlap. One person may hold both an
	// organization-wide owner membership and an owner membership scoped to this
	// workspace — that pair is precisely how the defect was reachable — and a
	// recipient list naming them twice would write two inbox rows and send two
	// mails.
	//
	// The address comes back with the id because both deliveries address the same
	// person: the inbox row is keyed by user, the mail by address, and looking the
	// second one up separately would mean a query per recipient.
	ListUsersWithRoleInOrg(ctx context.Context, arg ListUsersWithRoleInOrgParams) ([]ListUsersWithRoleInOrgRow, error)
	// A link's whole split, including the disabled arms and the fallback, in
	// rotation order.
	//
	// Ordered by the destination's position rather than by created_at: position is
	// assigned once, from NextRuleDestinationPosition, and is the only ordering that
	// a person reading the list can predict and that a rotation can be explained
	// against. Two arms created in the same transaction cannot tie on it.
	ListVariantRules(ctx context.Context, arg ListVariantRulesParams) ([]ListVariantRulesRow, error)
	// Verification and serving (M40).
	//
	// The gate this milestone exists for is one column: `verified_at`. Nothing below
	// lets a hostname be served without it, and the two statements that clear it —
	// UnverifyDomain here, RenameDomain above — are the only ways serving stops.
	// Everything the host router may resolve aliases on, which is the whole of what
	// the in-process hostname cache holds.
	//
	// **`verified_at IS NOT NULL` is the gate.** A registered-but-unverified
	// hostname is absent from this result, so it is absent from the cache, so the
	// router has nothing to match a Host header against and the request lands on
	// ops-only 404. There is no second predicate anywhere that could disagree with
	// this one, because there is no second query.
	//
	// The instance default is excluded: it is matched on `is_default` at boot and
	// serves through the ordinary link host, and including it here would give one
	// hostname two routes into the redirect tree.
	//
	// Hostnames are lowered here as well as at lookup. `HostCache.Reload` runs each
	// one through `config.HostOnly`, which is what a Host header is spelled with, so
	// the two sides cannot disagree even if this SELECT stops lowering.
	ListVerifiedDomains(ctx context.Context) ([]ListVerifiedDomainsRow, error)
	//
	// One webhook's recent attempts, newest first, for the panel and the API. Bounded
	// by the caller: this is a log, and a page that renders all of it renders a log.
	ListWebhookDeliveries(ctx context.Context, arg ListWebhookDeliveriesParams) ([]ListWebhookDeliveriesRow, error)
	//
	// The secret is not selected. It is written once and read only by the signer, so
	// nothing that renders a page or answers the API can leak it by accident.
	ListWebhooks(ctx context.Context, workspaceID uuid.UUID) ([]ListWebhooksRow, error)
	// The workspace switcher: what a user may act in, and what they have chosen.
	//
	// Resolution itself is in auth.sql, because it is identity, not a feature.
	// These are the reads and writes the switcher and its account setting need.
	// Every workspace a user may act in, with the organization it belongs to.
	//
	// DISTINCT because a user can hold both an organization-wide membership and a
	// workspace-scoped one in the same organization, which the unique index
	// permits; without it the switcher would list a workspace twice.
	//
	// is_default is carried here rather than fetched separately so a page render
	// costs one query: the nav switcher needs the list, the account setting needs
	// the list plus which entry is pinned, and neither should cost two round trips.
	ListWorkspacesForUser(ctx context.Context, userID uuid.UUID) ([]ListWorkspacesForUserRow, error)
	// Serializes the setup flow's count-then-create.
	//
	// Both setup surfaces read CountUsers and then, in a separate transaction,
	// register the first user. Nothing held the gap, and the gap is wide: the
	// argon2 hash runs for ~100ms before the transaction even begins. On a fresh
	// closed instance, setup is unauthenticated and only login-rate-limited, so an
	// attacker polling it could have their CountUsers land in the window while the
	// real operator was hashing, and both would be created as "the first user" —
	// each with their own organization, on an instance the operator believes only
	// they can reach.
	//
	// Transaction-scoped, so it releases on commit or rollback with nothing to
	// clean up. The key is the ASCII bytes "lcsetup\0" as a literal, NOT a hash of
	// anything; to inspect it from psql use the value directly:
	//
	//     SELECT pg_advisory_xact_lock(7810213058373316608);
	LockFirstUserSetup(ctx context.Context) error
	//
	// The link row, locked, for a guard whose decision is a count.
	//
	// A count cannot be locked, so `SELECT count(*)` as a ceiling check is a
	// check-then-act: two writers each read a state the other is changing, and both
	// pass a limit neither would pass alone. This is `LockOrganizations`' pattern at
	// the link level — take the lock on the parent, make the decision inside the
	// transaction that writes, and let the second writer block until the first
	// commits and then re-read what it left behind.
	//
	// Locking the *link* rather than the rules is what makes it work with nothing to
	// lock in the empty case. Postgres takes `FOR KEY SHARE` on a parent when a row
	// referencing it is inserted, and that conflicts with `FOR UPDATE` — so a locked
	// link cannot acquire a routing rule or a split arm while the guard is deciding,
	// including the first one, where a lock on the rules would have had no rows to
	// take (F67).
	//
	// Returns the id alone: the caller already has the link, and this exists for its
	// lock rather than for its columns. No workspace parameter, for the same reason
	// — every caller has already been through GetLink, which is workspace-scoped, so
	// adding one here would be a second tenancy check in a statement whose job is
	// serialization.
	LockLink(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
	// The pending login behind a presented token, locked.
	//
	// Joined to `users` because every consumer needs both halves and the alternative
	// is two round trips with the account's state changing between them. `FOR UPDATE
	// OF p` locks the pending row and not the user row: the user row is locked
	// separately by the paths that write it, and locking it here would serialise every
	// second-factor attempt against every other write to the account.
	//
	// Nothing is filtered out. Expired, consumed, an account that stopped being active
	// while the prompt was open — each is a refusal the caller makes, and each is the
	// same refusal to whoever is looking at the form. Filtering here would collapse
	// them into not-found, which is the same answer, and would cost the tests their
	// ability to tell the five apart.
	LockMFAPendingLogin(ctx context.Context, tokenHash []byte) (LockMFAPendingLoginRow, error)
	// The organization's owner memberships, locked.
	//
	// Organization-wide only: a workspace-scoped owner membership grants ownership
	// of one workspace, not of the organization, so counting it would let the last
	// real owner be removed while a workspace-scoped row stood in for them.
	//
	// Rows rather than a count, because a count cannot be locked. Taken before any
	// removal or demotion of an owner, so two concurrent administrators cannot each
	// observe two owners and each remove one.
	LockOrganizationOwners(ctx context.Context, organizationID uuid.UUID) ([]uuid.UUID, error)
	// The organization's workspaces, locked, so no link can be created in one while
	// the link guard below is counting.
	//
	// Ordered for the same deadlock reason as LockOrganizations.
	LockOrganizationWorkspaces(ctx context.Context, organizationID uuid.UUID) ([]uuid.UUID, error)
	// Organization teardown (M28.5).
	//
	// Every statement here is either a guard or the delete it guards. The delete is
	// one line; the guards are the milestone.
	//
	// **Locking, and why each of these returns rows rather than a count.** A count
	// cannot be locked, so a guard written as `SELECT count(*)` is a check-then-act:
	// two administrators acting at once each read a state the other is changing.
	// The pattern is `LockOrganizationOwners`' — select the rows the decision is
	// made on `FOR UPDATE`, count them in Go, and let the second transaction block
	// until the first commits and then re-read what it left behind. Postgres also
	// gives this a second effect that is the point of it here: inserting a row that
	// references a locked parent takes `FOR KEY SHARE` on that parent, which
	// conflicts with `FOR UPDATE` — so a locked organization cannot acquire a new
	// workspace, and a locked workspace cannot acquire a new link, while the guard
	// is deciding.
	// Every live organization on the instance, locked, for the refusal that stops
	// the last one being deleted.
	//
	// Instance-wide rather than scoped, because that is what the rule is about: an
	// instance with no organization has no path back that does not involve SQL, the
	// same argument that refuses the last owner and the last workspace.
	//
	// `ORDER BY id` is load-bearing rather than cosmetic. Two concurrent deletions
	// of different organizations both take this lock, and taking a set of row locks
	// in a different order in each transaction is a deadlock; a fixed order makes it
	// a wait instead. It is also why this runs *before* the target row is read —
	// the target is inside this set, so it is already locked by the time anything
	// else touches it.
	LockOrganizations(ctx context.Context) ([]uuid.UUID, error)
	// The organizations this account owns alone, locked before they are counted.
	//
	// The refusal M28.5 makes from the other side. `team.guardOwnerSet` blocks
	// removing or demoting an organization's last owner; this blocks deleting the
	// *account* that is one, because otherwise the rule is bypassable by leaving
	// through a different door.
	//
	// **Organization-wide owner memberships only**, the sentence
	// `LockOrganizationOwners` states and for the same reason: a workspace-scoped
	// owner membership is ownership of one workspace, so counting it would either
	// hide a sole owner or invent one.
	//
	// The owner rows are locked before the count is taken, so this is a rule rather
	// than a check-then-act — a second administrator promoting or removing an owner
	// blocks until this transaction ends. Ordered by membership id inside the
	// locking CTE so two accounts deleting themselves at once take the rows in the
	// same order and cannot deadlock. MATERIALIZED because the lock has to be taken
	// once, on the rows this counts, rather than folded into the outer query by the
	// planner.
	LockOrganizationsSolelyOwnedBy(ctx context.Context, userID uuid.UUID) ([]LockOrganizationsSolelyOwnedByRow, error)
	// Account deletion and subject erasure (M52).
	//
	// Two operations that look like one and are deliberately kept apart. **Deletion**
	// is interactive, immediate and one transaction: it ends every route into the
	// account and releases the address. **Erasure** is the hourly sweep that scrubs
	// what deletion could not reach — every record *about* this person that ending
	// the account does not remove. *No foreign key to `users`* was that criterion
	// while the sweep touched two tables and is not the criterion now: it reaches
	// four, two of which do carry one. The enumeration lives on
	// `EraseDeletedAccounts` below, beside the statement, and nowhere else.
	//
	// The statements below are the whole of the database side. `04000` adds the one
	// index the sweep reads and nothing else; the columns have existed, unwritten,
	// since `00200_identity.sql`.
	// The account being deleted, locked for the rest of the transaction.
	//
	// `deleted_at IS NULL` is in the predicate rather than checked afterwards, so a
	// second deletion of the same account is not-found instead of a second pass over
	// rows the first one already took. Two browsers pressing the button at once is
	// the ordinary way that happens.
	LockUserForDeletion(ctx context.Context, id uuid.UUID) (User, error)
	// The same row, locked for the rest of the transaction.
	//
	// Every write below reads through this first. Enrolment, disabling and accepting
	// a code all read a column and then write it, and two of those are the difference
	// between a second factor existing and not — a check-then-act on `mfa_enabled_at`
	// is how an abandoned enrolment and a live one end up in the same account.
	LockUserMFA(ctx context.Context, id uuid.UUID) (LockUserMFARow, error)
	//
	// Every folder in a workspace, locked, for a decision made over the whole tree.
	//
	// `MoveFolder`'s refusals are computed in Go from a tree read a moment earlier —
	// *is the new parent inside the subtree being moved* cannot be written as a
	// column check — so the read and the write have to be one transaction or two
	// concurrent moves each decide against a tree the other is changing. Moving A
	// under B while B moves under A passes both checks and produces the cycle M38
	// says can never exist (F108).
	//
	// The whole workspace rather than the two rows involved, because the predicate
	// is over the whole tree: a cycle can run through folders neither move names.
	// Workspaces are small enough for that to be one indexed read.
	//
	// `ORDER BY id` is load-bearing rather than cosmetic, and it is
	// `LockOrganizations`' reasoning: two transactions taking the same set of row
	// locks in different orders deadlock, and a fixed order makes it a wait instead.
	//
	// Returns ids alone. The caller reads the tree it decides on through
	// ListFolders inside the same transaction; this exists for its lock.
	LockWorkspaceFolders(ctx context.Context, workspaceID uuid.UUID) ([]uuid.UUID, error)
	// Closes the predecessor: names its successor and sets the far edge of the
	// grace window.
	//
	// `successor_id IS NULL` in the WHERE clause is belt to the FOR UPDATE braces.
	// The lock is what serializes; this is what makes the second writer a no-op
	// rather than a silent overwrite if the lock is ever dropped from the read.
	MarkAPIKeyRotated(ctx context.Context, arg MarkAPIKeyRotatedParams) (int64, error)
	MarkAllNotificationsRead(ctx context.Context, userID uuid.UUID) (int64, error)
	//
	// The cursor advance, for the rules one run actually looked at.
	//
	// One statement per run rather than one per rule, so the whole of the fairness
	// mechanism costs a single indexed update however many rules were considered —
	// the per-run bound m43.md asks for is a product of four constants plus this.
	//
	// **It writes `last_checked_at` and nothing else.** Not `last_fired_at`: a rule
	// that matched nothing, or matched less than its threshold, has not fired, and
	// moving its watermark would discard the subjects already inside its window.
	// Not `updated_at` either — this is the scheduler's bookkeeping rather than an
	// edit somebody made, and touching it would make every rule on the instance look
	// edited once a minute.
	//
	// Rules whose evaluation *failed* are in the list on purpose. A rule with a
	// corrupt `actions` column errors on every pass; leaving its cursor where it was
	// would park it at the head of the queue forever, which is F83 again with a
	// different cause.
	MarkAutomationRulesChecked(ctx context.Context, arg MarkAutomationRulesCheckedParams) error
	// The second half, and never run on its own: without the clear before it, it is
	// the collision the comment above describes.
	MarkDefaultQRCode(ctx context.Context, arg MarkDefaultQRCodeParams) (int64, error)
	// Caddy asked whether to obtain a certificate for this hostname and was told
	// yes. Guarded on 'pending' so it is one write per verification rather than one
	// per handshake: the ask endpoint is public and unauthenticated, and a statement
	// it could run on every request would be a write amplifier anybody can pull.
	MarkDomainTLSActive(ctx context.Context, id uuid.UUID) (int64, error)
	// A failed check, and deliberately *not* a stop.
	//
	// verified_at is untouched: a domain that is serving goes on serving while the
	// grace window runs (D70). What this records is that the window has started —
	// COALESCE keeps the first failure's timestamp, so a run of failures anchors on
	// when the run began rather than sliding forward with every poll, which would
	// make the window unreachable.
	//
	// **The hostname and the token are in the predicate for the reason
	// MarkDomainVerified carries them (M40, reopened; F131).** This is the same
	// read-check-write, over the same gap held open by the same nameserver, and the
	// rename that commits inside it is the same rename. What a late write lands here
	// is smaller than what it lands there and it is not nothing: a
	// `verification_error` sentence naming the *old* hostname, shown on the Domains
	// page against the new one, and a `verification_checked_at` the new name never
	// earned — which moves the row out of the head of the pending queue that
	// `verificationWorkList` orders NULLs first, so the name nobody has checked is
	// checked later than it should be. Predicating on what was proved makes the late
	// write affect zero rows, and the caller treats that as the conflict it is.
	//
	// It also closes the statement below transitively on the job's path. A failed
	// check that finds no row to land on returns before UnverifyDomain is reached, so
	// the grace window cannot expire against a hostname this pass never checked —
	// UnverifyDomain's own `verified_at IS NOT NULL` was already refusing that write,
	// and now nothing gets that far to be refused.
	MarkDomainVerificationFailed(ctx context.Context, arg MarkDomainVerificationFailedParams) (Domain, error)
	// A successful check, written only onto the row that was actually checked.
	//
	// **The hostname and the token are in the predicate because they are what was
	// proved (M40, reopened).** Verification reads a row, resolves DNS against the
	// hostname it read — seconds, against a nameserver the registrant runs — and then
	// writes here. `RenameDomain` is concurrently reachable and clears `verified_at`;
	// an unconditional write landing after it would fill exactly that NULL through
	// the COALESCE below and start serving a name nobody proved, up to and including
	// one of the instance's own hosts. Predicating on `hostname` and
	// `verification_token` makes the late write affect **zero rows** instead, and the
	// caller treats that as the conflict it is.
	//
	// A transaction around the read and the write would not have closed this: the
	// rename commits in the gap between two separate transactions, so there is
	// nothing for it to serialise against. `FOR UPDATE` across the lookup would have,
	// by pinning a row lock for the DNS timeout inside a job that walks a batch —
	// which is a different outage.
	//
	// Sets verified_at only when it is not already set, so a domain that has been
	// serving for a month does not have its start date rewritten every hour — the
	// column answers "since when has this been served", and a re-check is not a new
	// answer.
	//
	// ssl_status moves to 'pending': the app never speaks ACME (decision D3), so the
	// most it can say is that it will now answer Caddy's on-demand ask for this
	// hostname. 'active' is written by that ask endpoint, once, when it is first
	// consulted.
	//
	// The failing streak is cleared unconditionally, which is D70's "a successful
	// check at any point resets the count".
	MarkDomainVerified(ctx context.Context, arg MarkDomainVerifiedParams) (Domain, error)
	// The single-use write. Conditional on the invite still being redeemable, so
	// even without the lock above this could not be spent twice.
	MarkInvitationRedeemed(ctx context.Context, arg MarkInvitationRedeemedParams) (int64, error)
	//
	// Retry exhausted. Terminal, and deliberately not deleted — a row that says
	// what was attempted and why it never arrived is the whole point of an outbox
	// over an in-memory retry loop.
	//
	// Blanked for F32's reason, and the failed case is the one that needs it most: a
	// message that never arrived is one whose token is still unspent, and this row
	// would otherwise hold it for thirty days against an invitation that lives seven.
	// The retry path deliberately does not blank — a row still being retried is
	// pending, and it has to keep the message it is going to send.
	MarkMailFailed(ctx context.Context, arg MarkMailFailedParams) error
	//
	// A failure that will be tried again. The error is kept verbatim: it is what an
	// operator reads when somebody reports that mail never arrived.
	//
	// Replaces the lease ClaimDueMail set with the real backoff for this attempt,
	// and does not touch attempts — the claim already spent it.
	//
	// Seconds rather than an interval parameter, matching the lockout query in
	// auth.sql: an interval maps to pgtype.Interval, which would put a driver type
	// in the service layer's signature for no benefit.
	MarkMailRetry(ctx context.Context, arg MarkMailRetryParams) error
	//
	// attempts is not touched: ClaimDueMail already spent it.
	//
	// The body is blanked here, in the same statement that marks the row sent
	// (finding F32). Two of the templates this phase ships carry a single-use token
	// in their body, and a delivered message is one whose token has reached the only
	// person entitled to it — keeping a copy afterwards is keeping a redeemable
	// credential in clear for the retention window. Folded into this UPDATE rather
	// than done after it, so there is no crash window in which a row is sent and
	// still carries the token, and so `mail_outbox_finished_body_scrubbed` cannot be
	// passed by a caller that forgets. What an operator reads afterwards —
	// recipient, subject, kind, attempts, last_error — is untouched.
	MarkMailSent(ctx context.Context, id uuid.UUID) error
	//
	// Scoped by user_id as well as id, so someone else's notification is a
	// zero-row update rather than a 403 that confirms the id exists.
	//
	// read_at is only set once. Marking an already-read notification is a no-op
	// rather than a fresh timestamp, so "when did you first see this" survives a
	// double click.
	MarkNotificationRead(ctx context.Context, arg MarkNotificationReadParams) (int64, error)
	//
	// `read_at` back to NULL, which is the whole of "unread": 00600 declared the
	// column nullable and the inbox has always used NULL for it, so putting one
	// back is an UPDATE and never a migration (M48).
	//
	// **Deliberately not the mirror image of MarkNotificationRead.** That statement
	// refuses to touch an already-read row so that "when did you first see this"
	// survives a double click. This one carries no such guard: it exists because the
	// click-through M48 adds marks a notification read as a side effect of opening
	// it, and somebody undoing that is saying they have not dealt with it — which is
	// as true of a row read last week as of one read by accident a second ago. The
	// first-seen timestamp is what is being discarded, on purpose, by the person it
	// belongs to.
	MarkNotificationUnread(ctx context.Context, arg MarkNotificationUnreadParams) (int64, error)
	//
	// Retry exhausted. Terminal, and deliberately not deleted: a row saying what was
	// attempted, how many times, and what the receiver said is the whole reason this
	// is a table rather than an in-memory retry loop.
	MarkWebhookAbandoned(ctx context.Context, arg MarkWebhookAbandonedParams) error
	//
	// attempts is not touched: the claim already spent it.
	MarkWebhookDelivered(ctx context.Context, arg MarkWebhookDeliveredParams) error
	//
	// A failure that will be tried again. Replaces the lease the claim set with the
	// real backoff for this attempt, and does not touch attempts.
	//
	// response_code is nullable and stays NULL when there was no response at all,
	// which is what tells a refused connection apart from a receiver answering 500.
	MarkWebhookRetry(ctx context.Context, arg MarkWebhookRetryParams) error
	// The low-confidence destination blocklist (M30).
	//
	// Read on the management path only. The redirect tree never touches this table:
	// blocking decides what may be stored, not what may be served, and putting a
	// query here on the hot path would buy nothing a link that was refused at
	// creation does not already have.
	// Whether any of a host's label-boundary suffixes is on the list.
	//
	// The caller passes the full host and every parent of it — a.b.example becomes
	// {a.b.example, b.example, example} — so the label-boundary rule is enforced by
	// what is asked for rather than by a pattern match, and the whole question is
	// one index probe. Longest first in the caller, and ORDER BY length here, so a
	// specific entry wins over the parent it sits under and the reason an operator
	// reads is the one they wrote for that host.
	//
	// The source comes back because it decides which rule the refusal reports: a
	// seeded shortener says shortener_chain and everything else says
	// operator_blocklist. One row rather than one query per source — a host that is
	// both listed by the operator and a known shortener is one refusal, and the
	// more specific entry is the longer one, which this already returns.
	MatchBlockedDestination(ctx context.Context, candidates []string) (MatchBlockedDestinationRow, error)
	//
	// Links whose durable click budget ran out inside the window (M35).
	//
	// `link_click_budget.exhausted_at` is stamped in the same transaction that
	// spends the last click, so it is an exact event time. `links.click_count` is
	// not, and 02100 says out loud that it must never be an authorization input;
	// it is not one here either.
	//
	// Walks link_click_budget_exhausted_idx (02100) — declared DESC, read forward,
	// which Postgres serves from the same index backwards.
	//
	// The lower bound is the composite watermark, in the same range-plus-tiebreak
	// spelling MatchExpiredLinks explains. Ties on `exhausted_at` are rarer here
	// than on expiry — each stamp is its own transaction — but two budgets spent in
	// the same microsecond are not impossible, and the cursor costs nothing.
	MatchExhaustedBudgets(ctx context.Context, arg MatchExhaustedBudgetsParams) ([]MatchExhaustedBudgetsRow, error)
	//
	// Links whose expiry fell inside this rule's window.
	//
	// **No status filter, on purpose.** `archive_link` is one of the actions a rule
	// may take, so a trigger that excluded archived links would shrink its own match
	// set when it fired — a rule feeding off its own effect, which is exactly what
	// domain/automation.go's TriggerReads/ActionWrites declaration says cannot
	// happen. `links_expiry_idx` (00300) is unusable here for that reason: its
	// predicate carries `status = 'active'`. 02900 adds automation_links_expiry_idx,
	// which does not.
	//
	// Soft-deleted links are excluded, and that exclusion is safe for the opposite
	// reason: nothing an automation does writes `deleted_at`.
	//
	// The lower bound is the composite watermark, and its spelling is deliberate:
	// `expires_at >= @after` plus a tiebreak is the same predicate as
	// `(expires_at, id) > (@after, @after_subject)`, written so the planner keeps a
	// single range condition to walk automation_links_expiry_idx with and applies
	// the tiebreak as a filter on the handful of boundary rows. Strict `>` on the
	// timestamp alone is the shape this replaces, and it was lossy: a capped fetch
	// that stopped inside a group of links sharing one expiry — bulk creation makes
	// those routine — left the tied remainder outside every later window, forever.
	MatchExpiredLinks(ctx context.Context, arg MatchExpiredLinksParams) ([]MatchExpiredLinksRow, error)
	//
	// Administrative events of one action inside the window.
	//
	// The blocked-attempt trigger's source, and it reads the audit log because that
	// is the only durable trace a refusal leaves — `blocked_destinations` (01500) is
	// the operator's blocklist, not a record of attempts against it.
	//
	// The action is a parameter with exactly one caller, which passes
	// domain.TriggerDestinationBlocked. The three names for that event — the
	// trigger, the audit action and the webhook event — are the same string, and
	// TestTheBlockedVocabularyIsOneWord holds them together.
	//
	// Walks audit_logs_workspace_action_idx (02900).
	//
	// The lower bound is the composite watermark, in the same range-plus-tiebreak
	// spelling MatchExpiredLinks explains. `occurred_at` carries whatever instant
	// the recorder was handed, so a burst of refusals in one transaction shares a
	// timestamp the way bulk-created links share an expiry.
	MatchWorkspaceAuditEvents(ctx context.Context, arg MatchWorkspaceAuditEventsParams) ([]MatchWorkspaceAuditEventsRow, error)
	// The parent is set outright rather than COALESCEd, because NULL is a
	// destination here: it means the root. A partial-update idiom would make
	// "move to the top level" unexpressible.
	MoveFolder(ctx context.Context, arg MoveFolderParams) (Folder, error)
	// Gives a slug to the one code that may not have one (M50's reopening, D183).
	//
	// A link's only code carries no slug: there is nothing to tell it apart from,
	// and handing one out while writing a style would change what a picture says.
	// When a second code appears the first one needs a tag, and this is the
	// statement that writes it.
	//
	// **`AND slug = ”` is what makes it structurally incapable of a rename.** A
	// slug is printed, so moving one breaks every copy already in the world —
	// UpdateQRCodeLabel says so above and this is the same rule enforced by the
	// WHERE clause rather than by the caller. Naming a code that has no name is not
	// moving anything: nothing printed carries the value being replaced, because
	// there was no value.
	//
	// **`is_default = true` goes with the slug, and it is the one statement in this
	// file that sets the flag without clearing another.** The row this reaches is
	// whichever row GetDefaultQRCode answered with, and that read falls back to the
	// empty slug for a row the flag never reached — one written by the previous
	// release during a rolling deploy, where `is_default` is a column it does not
	// know about. Taking the empty slug off such a row without putting the flag on
	// it would leave the link matching neither half of `(is_default OR slug = ”)`:
	// no default at all, a phantom code synthesised into every list and breakdown,
	// and the untagged `qr` bucket no longer folding onto the code every already-
	// printed picture of this link resolves through. The empty slug and the flag are
	// two spellings of the same fact, so the statement that removes one writes the
	// other.
	//
	// Against `qr_codes_link_default_key` this is safe by the same read: it runs
	// only on a row carrying the empty slug, and GetDefaultQRCode orders the flag
	// first, so a link with some *other* row flagged never returns this row to be
	// named. What the read cannot rule out is the flag moving between it and this
	// write, which is a unique violation and is the caller's to answer — CreateQRCode
	// re-reads the winner rather than failing over it.
	NameQRCode(ctx context.Context, arg NameQRCodeParams) (int64, error)
	// The next free position above the primary. COALESCE so the first rule on a
	// link lands at 1 rather than at NULL.
	NextRuleDestinationPosition(ctx context.Context, linkID uuid.UUID) (int32, error)
	// Advance a link's sequential rotation and return the position it advanced to
	// (M36, D8).
	//
	// The same table, the same upsert shape and the same concurrency argument as
	// ConsumeClickBudget: the statement is the transaction, two replicas serialise
	// on the row lock the ON CONFLICT path takes, and the loser is evaluated against
	// the winner's committed value. That is what makes the order strict *globally*
	// rather than per process — an in-memory counter would give each replica its own
	// rotation and "sequential" would mean "sequential here", which is a support
	// ticket rather than a feature.
	//
	// A different column from `consumed` on purpose. A rotation advances; a budget
	// is spent and refuses when it runs out. Sharing one number would let a
	// sequential arm consume a one-time link's single click on its way to being
	// chosen, and the gate that runs afterwards would find the link already spent.
	//
	// Unconditional: there is no limit to reach, so unlike ConsumeClickBudget this
	// always returns a row. The write lands only on links that actually carry a
	// sequential arm, which is the cost D8 accepts and the reason every other link
	// keeps the unchanged fast path.
	NextVariantRotation(ctx context.Context, arg NextVariantRotationParams) (int64, error)
	// The code that has existed longest, which is what a removed default promotes to
	// (M50's reopening).
	//
	// `created_at, id` is the order ResolveAliasForRedirect enumerates codes in, and
	// the oldest code is the one whose pictures have been in the world longest —
	// which is what makes it the right one to inherit every untagged scan.
	//
	// **The rule is unchanged and the sentence that explained it is** (M50.8). This
	// comment used to add *"so the promoted code is the one at the top of the list
	// the reader is looking at"*, which was true while ListQRCodes ordered by
	// creation. That list is alphabetical now, so the promoted code is wherever its
	// name puts it. D183 chose the oldest and nothing here reverses that; what went
	// is a claim about where the reader's eye lands, which had stopped being true.
	//
	// Excluding a row by id rather than filtering on
	// `is_default`, because the caller runs this *after* deleting the flag-holder in
	// the same transaction and inside it that row is already gone — the exclusion is
	// what makes the statement correct if it is ever called before one.
	OldestQRCode(ctx context.Context, arg OldestQRCodeParams) (OldestQRCodeRow, error)
	// The same lookup without the lock, for rendering the redemption page.
	//
	// A GET must not take a row lock: the page is served to anybody holding the
	// link, and a locking read there would let a stranger hold a write lock on the
	// row by opening a page.
	PeekInvitationByTokenHash(ctx context.Context, tokenHash []byte) (PeekInvitationByTokenHashRow, error)
	// Read a link's rotation without advancing it (F100).
	//
	// The read-only twin of NextVariantRotation, and it exists for one caller:
	// HEAD. A link checker or an unfurler probing a sequentially split link used to
	// advance the durable counter on every probe, re-phasing every subsequent
	// visitor's arm — and because HEAD writes no click event, the per-destination
	// breakdown could not show why the arms were uneven.
	//
	// Returning early on HEAD is *not* the fix: a HEAD would then answer the link's
	// own destination while a GET answers an arm, so a checker would validate a URL
	// no visitor is ever sent to. HEAD has to choose the same arm the next GET
	// would, which is what this reads.
	//
	// The same shape as Budget, which reads a click allowance without spending it
	// for exactly the same caller and the same reason. No row means no click has
	// landed yet, and the caller treats that as position 1 — the first arm, which is
	// what the first visitor will get.
	PeekVariantRotation(ctx context.Context, arg PeekVariantRotationParams) (int64, error)
	// The end of the trash window: hard-delete links whose purge_after has passed.
	//
	// One statement, so the reservation and the deletion cannot be separated by a
	// crash: an alias that ever received traffic is written to reserved_aliases in
	// the same command that removes its row, and ON CONFLICT makes a retried run
	// converge rather than fail. Aliases that never received a click are released —
	// deliberately, per the reserved_aliases rationale: nothing in the wild points
	// at them, so permanent reservation would only bleed the namespace.
	//
	// SKIP LOCKED so the purge can never block, or be blocked by, a concurrent
	// restore-by-hand of the same row; a skipped row is caught on the next run.
	// Destinations and link_tags follow by ON DELETE CASCADE. click_events rows
	// carry no FK (partitioned) and are dropped by analytics retention instead.
	PurgeExpiredLinks(ctx context.Context, batchSize int32) ([]PurgeExpiredLinksRow, error)
	// The de-identification step. Once the salt is gone the day's hashes cannot be
	// linked back to an address.
	PurgeExpiredSalts(ctx context.Context) (int64, error)
	// The sweep. Lapsed rows and spent ones, in bounded batches, from the hourly
	// maintenance pass that already purges finished registrations and password
	// resets.
	//
	// No retention window, unlike those two. A spent pending login is evidence of
	// nothing — the session it minted is the record, and the audit trail carries the
	// rest — where a spent registration and a spent reset are each the only trace that
	// an address was proven.
	PurgeFinishedMFAPendingLogins(ctx context.Context, batch int32) (int64, error)
	//
	// Sent and failed rows past the retention window. The outbox is a record of
	// what was attempted, not an archive: without this the table is the one thing
	// in the schema that grows forever with no window and no metric, which is the
	// shape D5 and M21 exist to avoid repeating.
	//
	// It is a retention window and not a secrecy control. The rows it deletes lost
	// their bodies when they finished; this is what stops the record of *that* piling
	// up. Lowering it would not shorten any credential's exposure.
	PurgeFinishedMail(ctx context.Context, maxAgeDays int32) (int64, error)
	// The sweep. Removes tokens nobody used past their expiry, and spent rows past
	// the same short window a spent registration gets.
	//
	// Both, because neither is a record of anything a reader needs: the audit log
	// carries that the reset happened, and the password itself is the durable
	// evidence. This table is a waiting room, not an archive.
	PurgeFinishedPasswordResets(ctx context.Context, arg PurgeFinishedPasswordResetsParams) (int64, error)
	//
	// Delivered and abandoned rows past the retention window. The delivery log is a
	// record of what was attempted, not an archive; without this it is a table that
	// grows forever with one row per link write per webhook, which is the shape D5
	// and M21 exist to stop repeating.
	PurgeFinishedWebhookDeliveries(ctx context.Context, maxAgeDays int32) (int64, error)
	// The sweep. Removes registrations nobody completed, and consumed rows whose
	// account has long since been created.
	//
	// Both, because neither is a record of anything: an account that exists is
	// evidence enough that its address was proven, and the audit log carries what
	// happened. This table is a waiting room, not an archive — the one shape it
	// must not have is the unbounded growth D5 and M21 exist to stop repeating.
	PurgeLapsedRegistrations(ctx context.Context, keepDays int32) (int64, error)
	// Returns the new count so the caller can apply the lockout policy without a
	// second round trip and without a read-modify-write race between two
	// concurrent attempts.
	//
	// An elapsed lockout starts the count over. Incrementing unconditionally meant
	// the counter only ever went down on a successful sign-in or a password change,
	// so once an account had been locked it sat at the threshold forever: the user
	// waited out the window, got one attempt, and a single wrong guess re-locked
	// them for the full duration. The lockout became permanent for anyone who could
	// not remember their password on the first try — which is the population it
	// applies to.
	RecordFailedLogin(ctx context.Context, arg RecordFailedLoginParams) (RecordFailedLoginRow, error)
	// Keeps the watermark where it was: a failed run has not covered its window,
	// and advancing past it would turn one bad run into permanent gaps. Keeps
	// last_success_at where it was for the same reason — the last success is a fact
	// about the past that a later failure does not change, and it is what the
	// staleness gauge measures against.
	RecordJobFailure(ctx context.Context, arg RecordJobFailureParams) error
	RecordSuccessfulLogin(ctx context.Context, id uuid.UUID) error
	// The hostname is the only thing a registration has to change, and it is
	// changeable only while nothing serves it; see decisions.md, D69.
	//
	// Not scoped by owner. The caller has already been judged against the row read
	// by GetDomainByID, and repeating the predicate here would turn a 403 into a
	// 404 for anybody who got past that check by a route this file cannot see.
	//
	// **A rename un-verifies (M40), and that is the bullet D69 deferred to here.**
	// The proof of control is a TXT record published under the *old* name and says
	// nothing about the new one, so carrying verified_at across would let a
	// workspace verify a name it controls and then rename the row to one it does
	// not. The token is minted afresh for the same reason: the old value is
	// published in somebody else's zone.
	RenameDomain(ctx context.Context, arg RenameDomainParams) (Domain, error)
	RenameFolder(ctx context.Context, arg RenameFolderParams) (Folder, error)
	// Name and slug move together. The slug is derived from the name by the caller
	// rather than kept as a separate field somebody can edit into disagreement with
	// it, and the partial unique index on (organization_id, lower(slug)) is what
	// refuses a collision.
	RenameWorkspace(ctx context.Context, arg RenameWorkspaceParams) (Workspace, error)
	// Called before purging a link that has clicks. The alias is in the wild — on
	// printed material and in other people's bookmarks — so handing it to a new
	// destination would be a redirect hijack.
	ReserveAlias(ctx context.Context, arg ReserveAliasParams) error
	// The organization-level half of the same reservation, and it exists for the
	// same reason the link guard exists one level up: an organization-level cascade
	// that behaved differently from a workspace-level one would make the rule
	// bypassable by deleting one level up.
	//
	// The rationale is written out in full beside
	// ReserveWorkspaceTraffickedAliases; only the reach differs. Note what is
	// deliberately *not* filtered here: workspaces are joined without
	// `deleted_at IS NULL`, unlike CountOrganizationLinks, because this statement
	// follows what the cascade takes rather than what the guard counted, and the
	// cascade takes every workspace of the organization.
	ReserveOrganizationTraffickedAliases(ctx context.Context, organizationID uuid.UUID) error
	// Run immediately before DeleteWorkspace, in the same transaction, because the
	// cascade below is the third way a link lets go of its alias and it was the one
	// that let go for free (F28).
	//
	// CountWorkspaceLinks excludes soft-deleted links deliberately — counting them
	// would leave a workspace undeletable until the purge job ran — so a workspace
	// reaching the delete may still hold trashed links, for up to the trash window.
	// The cascade hard-deletes them without the purge job ever seeing them, and
	// `IsAliasTaken` then stops finding the row: an alias that was on printed
	// material yesterday is claimable by anyone on the instance today.
	//
	// `click_count > 0` is PurgeExpiredLinks' threshold, and the rename path's, and
	// it is the same threshold on purpose: three paths that release an alias must
	// not hold three opinions about what "in the wild" means. Aliases that never
	// received a click are released, per the reserved_aliases rationale.
	//
	// No deleted_at predicate, and FOR UPDATE, for one reason between them. The
	// guard above has already established there are no live links, so this selects
	// exactly the trashed ones — unless a row stopped being trashed after the count
	// ran, which today takes a hand-written UPDATE because nothing in the product
	// un-trashes a link (`RestoreLink` restores an *archived* one and requires
	// `deleted_at IS NULL`). The statement follows what the cascade will take
	// rather than what the guard counted, so such a row is reserved rather than
	// skipped, and the lock makes it wait rather than slip between the two.
	ReserveWorkspaceTraffickedAliases(ctx context.Context, workspaceID uuid.UUID) error
	// The redirect hot path.
	//
	// Everything here runs under a 20ms budget on the dedicated redirect pool.
	// Keep the query set small, index-covered, and free of joins that are not
	// strictly required.
	// Single-row lookup on links_domain_alias_key.
	//
	// primary_url is read from the denormalized column rather than joined from
	// destinations: the join would double the row fetches on the hottest query in
	// the system to retrieve a value a trigger already keeps in step.
	//
	// Status and expiry are returned rather than filtered, so the handler can
	// distinguish 404 (unknown or archived) from 410 (expired) and can cache a
	// negative result. Filtering here would make every non-serving state look
	// identical.
	//
	// The one join this query has, and the rule above is why it reads the way it
	// does. M32.5 needs the domain's bot-blocking settings on the redirect path,
	// and the alternative — a second lookup, or a second cache with its own
	// invalidation — would put either an extra round trip or an extra staleness
	// window on the hottest path in the product. This is neither: `domain_id` is
	// the domains primary key, the table holds one row on every deployment built so
	// far, and the two booleans ride home inside the round trip that was happening
	// anyway. The cached snapshot then carries them, so a cache hit answers the
	// whole question — link policy and domain policy together — without asking
	// anything.
	//
	// No `d.deleted_at IS NULL` here, deliberately. A soft-deleted domain row still
	// joins, which is exactly the behaviour this query had before the join existed;
	// adding the filter would silently turn every link on such a domain into a 404,
	// which is a change nobody asked this milestone to make.
	//
	// The lateral is M34's, and it obeys the same rule the domain join does: the
	// routing rules a link carries have to be in the snapshot, and the alternatives
	// are a second query on every cache miss or a second cache with its own
	// invalidation. This is neither. It is an index probe on
	// `routing_rules_link_idx` — partial on `enabled`, keyed on (link_id,
	// priority) — which finds nothing at all for the overwhelming majority of
	// links, because a link with no rules is the default and always will be.
	// Nothing about it runs on a cache *hit*: by then the rules are already inside
	// the snapshot.
	//
	// The rules come back as one jsonb array, already in evaluation order, because
	// ordering them here is free and ordering them in Go would mean the sort that
	// decides which destination a visitor gets lived somewhere other than the query
	// that reads them. The keys are spelled out rather than short: this is the
	// database's own vocabulary, and the compact spelling the cached snapshot uses
	// is the Go type's business.
	//
	// M36 widened the lateral to every kind and left it a single probe, which is the
	// property worth protecting: a link's match rules and its split arms live in one
	// table, on one index, and asking for them separately would double the cost of
	// the only lookup a cache miss makes. The ordering carries both vocabularies at
	// once. `rr.kind <> 'match'` sorts false before true, so M34's rules come first
	// and keep their (priority, created_at) order exactly; the arms follow in
	// `dest.position` order, which is what a rotation is explained against and what
	// the dashboard lists. `created_at` remains the last tiebreak so the order is
	// total whatever else ties.
	//
	// `id` and `weight` are new here. The id is what a click is attributed to —
	// click_events.destination_id — and the weight is the arm's share; both have to
	// be in the snapshot because reading either at request time would be the query
	// this design exists to avoid.
	//
	// The second lateral is M50's, and it is the same bargain a third time. A link
	// may carry several QR codes, each printing a slug in its payload, and a scan
	// may only be attributed to a slug this link actually has — otherwise the
	// parameter is an open write surface into `link_dimension_daily`, reachable by
	// anybody who can read a URL. Checking that at request time would be a query on
	// the hot path; checking it against the snapshot is a scan of a slice bounded by
	// domain.MaxQRCodesPerLink. So the slugs ride home in the round trip that was
	// happening anyway, on `qr_codes_link_idx` — the index 03700 restored — and find
	// nothing at all for the overwhelming majority of links, because a link with no
	// named codes is the default and always will be.
	//
	// Slugs only, never labels or styles. The redirect path has no use for either: a
	// label is what a person reads in the dashboard and a style is how the picture
	// is drawn, and putting them in the snapshot would serialize workspace free text
	// into every cached entry for nothing.
	//
	// **`q.slug <> ”` stays, and what it excludes has shrunk to one row** (D183).
	// It used to leave the whole default code out, because the default *was* the row
	// with no slug. The flag carries that identity now and the row it used to be
	// gains a slug the moment a second code appears beside it, so a link's default
	// rides home like every other code and a payload naming it is matched. What is
	// still left out is a link's *only* code, which keeps the empty slug and the
	// untagged payload — and it is left out because there is nothing to match it
	// with: `Snapshot.CodeSlug` returns before it scans when the parameter is empty
	// or absent, so an empty string in this array could never be compared against
	// anything. Carrying it would serialize a byte no request can reach into every
	// cached entry for the majority of links, and falsify what `Snapshot.Codes`
	// promises about the payload a link with no named codes carries — which is the
	// premise CacheKeyVersion was not bumped on.
	//
	// **`is_default` deliberately does not.** A request carrying no `qrc` still
	// records the bare `qr` it has always recorded — the value on every row this
	// product has written since M41 — and which code that belongs to is a question
	// the breakdown answers when somebody reads it, from the flag as it stands then.
	// Resolving it here instead would put the answer in `link_dimension_daily`,
	// where moving the flag afterwards could not reach it, and would rewrite what
	// every pre-reopening scan is stored as for no gain a reader can see.
	ResolveAliasForRedirect(ctx context.Context, arg ResolveAliasForRedirectParams) (ResolveAliasForRedirectRow, error)
	// Read once at boot and cached. The default domain is matched on the flag
	// rather than on a hostname string, so it never has to agree with
	// LINKCTRL_BASE_URL.
	ResolveDefaultDomain(ctx context.Context) (ResolveDefaultDomainRow, error)
	// PHASE 2: custom domains. Present now because the cache key is already
	// host-scoped, so enabling it later needs no key change.
	ResolveDomainByHostname(ctx context.Context, lower string) (ResolveDomainByHostnameRow, error)
	// Which organization an **account-wide** key's request lands in (M54).
	//
	// One tier above ResolveWorkspaceForUser, and deliberately not a second copy of
	// it. That statement answers "which workspace, given a person and optionally a
	// bound"; this one answers "which tenant", and the answer then becomes M44's
	// organization_id bound so the workspace precedence stays stated exactly once.
	// Feeding it the organization the request resolved to is the whole of M44's
	// parameter surviving a key that has no organization column to feed it from.
	//
	// The rungs are the person's, for the reason D90 gives: what an unpinned key
	// follows is the person, and where the person is acting is part of that. Their
	// pinned default's organization wins, then the one they last used, then the
	// oldest they belong to. There is no session rung because there is no session.
	//
	// **Organization-wide memberships only.** An unpinned key has always required
	// one — GetAPIKeyByPrefix's predicate refuses a workspace-NULL key covered by a
	// workspace-scoped membership, and MayCreateOrgWide refuses to mint one without
	// it. Carrying that rule across the tenancy boundary is what stops an
	// account-wide key widening into an organization where its owner is scoped to a
	// single workspace: a key minted under organization-wide authority must not
	// acquire reach its owner could not have granted it there.
	//
	// Barred organizations are excluded here as well as in the coarse check, and
	// both matter: the coarse one decides whether the credential authenticates at
	// all, this one decides where it lands, and an administrator who cut their
	// tenant out must not be reachable by a key that simply had nowhere else to go.
	//
	// **The bar comes back with the answer** (F183). Cutting an organization out of
	// an account-wide key's reach stopped it *acting* there and not *reading about*
	// it: `auth.Service.Workspaces` bounded a key by pinned-or-not, and an
	// account-wide key is by definition not pinned, so the barred organization's
	// name, slug and workspace ids went on being listed to the revoked credential.
	// Closing that needs the whole barred set rather than the one organization this
	// request landed in, and this statement is the only place an account-wide key's
	// reach is already being resolved — so the set rides back with it as an array
	// and the read bound costs no query of its own. `barred` is aggregated in its
	// own CTE so it is computed once rather than per candidate organization, and the
	// exclusion below reads it instead of repeating the subquery: one expression of
	// the fact, which is what a reader of a security predicate should have to check.
	ResolveOrganizationForAPIKey(ctx context.Context, arg ResolveOrganizationForAPIKeyParams) (ResolveOrganizationForAPIKeyRow, error)
	// The workspace a request acts in, and the only place that question is
	// answered. Every identity — session, API key, CLI — comes through here.
	//
	// The precedence is the ORDER BY and nothing else, so there is one statement of
	// it rather than one per caller:
	//
	//   1. the session's own current workspace, for a request that has a session
	//   2. the workspace the user pinned as their default
	//   3. the workspace they used last
	//   4. the oldest workspace they are a member of
	//
	// Each rung is a tiebreak on the one above, so a user with a single membership
	// ties on all four and lands where they always did. That is what makes the
	// switcher a no-op for every instance that exists today.
	//
	// Membership is the WHERE clause, not the ordering, so a preference pointing at
	// a workspace the user has been removed from — or one that has been deleted —
	// cannot win. It simply stops matching and the next rung answers.
	//
	// session_id is optional. NULL leaves the LEFT JOIN unmatched and rung 1 dead,
	// which is right for a login (the session does not exist yet), the CLI, and an
	// API key. The join also requires the session to belong to this user, so a
	// borrowed id resolves nothing.
	//
	// organization_id is optional too, and it is a *bound* rather than a rung: it
	// narrows which workspaces are candidates without touching the precedence. Only
	// one caller passes it, and the reason is M44. An organization-wide API key is a
	// row with a NULL workspace_id, which means "every workspace in **the
	// organization** the key belongs to" — and without this clause the precedence
	// would happily rank a workspace in some *other* organization the owner also
	// belongs to, because membership is the only filter and a person's pinned
	// default is a property of the person rather than of the tenancy. The key would
	// then act in a tenant it was never issued for. Every other caller passes NULL
	// and resolves exactly as it always did.
	//
	// The organization's own deleted_at is checked as well as the workspace's, and
	// the two are not the same check. ListWorkspacesForUser has always filtered
	// both; this statement filtered only the workspace, so a workspace under a
	// soft-deleted organization could be resolved *into* and never *listed* — the
	// switcher would mark nothing selected, and a browser would show the first
	// entry while the session acted somewhere else. Latent rather than live, and
	// deliberately fixed anyway: DeleteOrganization is a hard DELETE today and
	// nothing in the tree sets organizations.deleted_at, so the two queries agree
	// in practice and would stop agreeing the moment anything soft-deletes an
	// organization. The asymmetry is invisible from either statement alone, which
	// is the reason it survived to be found by review rather than by a user (F25).
	ResolveWorkspaceForUser(ctx context.Context, arg ResolveWorkspaceForUserParams) (Workspace, error)
	RestoreLink(ctx context.Context, arg RestoreLinkParams) (Link, error)
	// Idempotent: revoking an already-revoked key keeps the original timestamp and
	// still reports one row, so a repeated call is a success rather than a 404
	// while a genuinely unknown id is still distinguishable.
	RevokeAPIKey(ctx context.Context, arg RevokeAPIKeyParams) (int64, error)
	// The administrator's revoke, keyed on the organization instead of on the
	// owner.
	//
	// RevokeAPIKey above is a person disabling their own credential and is the
	// normal path. This one exists because there was otherwise no path at all: a key
	// belonging to somebody else could be *seen* — the rotation records are
	// organization-scoped — and not stopped, so an administrator holding an incident
	// had to wait for its owner. Scoped by organization rather than by workspace
	// because a key is issued into an organization and its id is what an audit
	// record hands the reader.
	//
	// Returns the owner and the prefix rather than a row count, because this write
	// is audited and the record has to name whose credential was stopped. No row
	// means an id from another organization or none at all, and both answer the same
	// way at the call site.
	//
	// **Pinned keys only** since M54, and by the predicate rather than by a new
	// clause: `organization_id = $2` is never true of a NULL. That is the right
	// refusal rather than a gap. This statement destroys a credential outright, and
	// for a pinned key that is proportionate — the organization is the key's entire
	// reach, so cutting the reach and cutting the key are the same act. An
	// account-wide key belongs to an account that acts in tenants this administrator
	// has no authority over, and RevokeAPIKeyReachInOrganization below is what they
	// get instead.
	RevokeAPIKeyInOrganization(ctx context.Context, arg RevokeAPIKeyInOrganizationParams) (RevokeAPIKeyInOrganizationRow, error)
	// An administrator cutting their organization out of an account-wide key.
	//
	// Idempotent for the reason RevokeAPIKey is: the second call reports a row and
	// keeps the original timestamp, so repeating it is a success rather than a 404,
	// while an id nobody may act on is still distinguishable.
	//
	// ON CONFLICT rather than a prior existence check, because two administrators
	// reacting to the same incident is the normal case and neither should see an
	// error about the other.
	RevokeAPIKeyReachInOrganization(ctx context.Context, arg RevokeAPIKeyReachInOrganizationParams) (int64, error)
	// Used on password change. Anyone who had the old password must be logged out,
	// which is the entire point of changing it.
	// keep_session is optional: pass NULL to revoke everything, or the current
	// session's id to leave the browser the user is changing their password in
	// still signed in.
	RevokeAllUserSessions(ctx context.Context, arg RevokeAllUserSessionsParams) error
	// Withdraw one instance-level permission from one account.
	//
	// Returns the row count so the caller can tell "withdrawn" from "they never held
	// it" without a read first. Which permissions may travel this path at all is
	// decided in Go, not here: instance.admin is deliberately not one of them.
	RevokeInstancePermission(ctx context.Context, arg RevokeInstancePermissionParams) (int64, error)
	// Scoped by organization as well as id, so an id from another organization is
	// indistinguishable from one that does not exist: both change zero rows.
	//
	// Already-revoked and already-redeemed are excluded rather than tolerated. A
	// redeemed invite has produced a member, and reporting "revoked" for it would
	// claim something the tree does not support.
	RevokeInvitation(ctx context.Context, arg RevokeInvitationParams) (int64, error)
	// Auto-revocation, from housekeeping.
	//
	// revoked_at is set to the moment the window closed rather than to now(), so
	// the list says when the key stopped working instead of when the job noticed.
	// Nothing depends on this running: authentication already refuses a key past
	// grace_expires_at. What this buys is a key list that agrees with the behaviour.
	RevokeLapsedAPIKeyGraces(ctx context.Context) (int64, error)
	// Clears an expired invite out of the outstanding slot so a replacement can be
	// issued.
	//
	// The partial unique index cannot exclude expired rows — `now()` is not
	// immutable, so Postgres will not index on it — which means an invite that
	// lapsed still occupies the address. This runs immediately before the insert,
	// in the same transaction, and touches nothing that is still redeemable.
	RevokeLapsedInvitation(ctx context.Context, arg RevokeLapsedInvitationParams) (int64, error)
	RevokeSession(ctx context.Context, id uuid.UUID) error
	// The per-destination breakdown a split test is read from (M36).
	//
	// A pass of its own rather than a seventh row in RollupDimensionDaily's LATERAL
	// VALUES, and the reason is cost rather than tidiness. That expansion runs for
	// every click on the instance; adding a row to it would grow the sort and the
	// upsert count by a sixth, permanently, for a column that is NULL on every link
	// that runs no split test. Here the `destination_id IS NOT NULL` filter is served
	// by the partial index migration 02200 creates, so on an instance with no split
	// tests this reads an empty index and writes nothing.
	//
	// The value is the destination id as text, into the same `link_dimension_daily`
	// table under the dimension name `destination`, so the breakdown is capped,
	// rolled up and read by exactly the query every other breakdown is read by. The
	// reader resolves ids to URLs; storing the URL here instead would freeze it at
	// the moment of the rollup and make an edited destination look like two.
	//
	// Bots excluded, like every other dimension: a split test scored on crawler
	// traffic is a split test with a wrong answer.
	RollupDestinationDaily(ctx context.Context, arg RollupDestinationDailyParams) error
	// Every dimension in one pass over click_events.
	//
	// This was six UNION ALL branches, one per dimension, reading the same rows six
	// times. Measured on the load-test dataset (5.7M events, ~830k inside the
	// recomputed window), that shape sorted 6.2M rows through an external merge that
	// spilled 471 MB of temp files, every 60 seconds. Reading once and expanding each
	// row with LATERAL VALUES lets the sort use the index's link_id ordering, so it
	// runs incrementally in memory instead — peak 152 kB per group, no temp files.
	//
	// Wall clock is unchanged (~20s either way), and that is the finding rather than a
	// disappointment: the time is in the 553k upserts a whole-day recompute implies,
	// not in reading the events. See docs/slo.md. This version is kept because
	// eliminating half a gigabyte of temp I/O per run is worth having on any host
	// smaller than the one it was measured on; it is not a fix for the job's cost.
	//
	// The output is identical: same grouping keys, same aggregates, same conflict
	// resolution. TestDimensionRollupMatchesAPerDimensionAggregate checks that
	// against a per-dimension aggregate written the other way round.
	RollupDimensionDaily(ctx context.Context, arg RollupDimensionDailyParams) error
	// Recompute per-link daily totals for a window.
	//
	// Idempotent by construction: it recomputes a whole day from the raw events
	// and upserts, so running it twice, or after a crash mid-run, converges to the
	// same numbers. An incremental "add what is new" design would double-count on
	// any retry.
	RollupLinkDaily(ctx context.Context, arg RollupLinkDailyParams) error
	RollupWorkspaceDaily(ctx context.Context, arg RollupWorkspaceDailyParams) error
	RotateWebhookSecret(ctx context.Context, arg RotateWebhookSecretParams) error
	// Both switches at once, because they are one setting with two halves and the
	// CHECK in 01800 refuses the combination that writing them separately would pass
	// through on the way. That applies row by row, so a propagation that touched one
	// column would be refused by the constraint on the way out.
	//
	// **Every undeleted domain, not only the default (F89).** This was
	// `WHERE is_default` until M45, which was the whole truth while the instance
	// default was the only domain there was. M40 added verified custom hostnames and
	// `ResolveAliasForRedirect` reads the policy from the link's *own* domain row, so
	// an operator who turned blocking on — even enforced — got no blocking on any
	// link served on a custom hostname, and any workspace could open that hole for
	// itself by registering one. Plan.md's "the domain's setting is instance-wide" is
	// the claim being restored, and this is what makes it true.
	//
	// **Every updated row comes back, not just the default**, because each one is a
	// cache invalidation the caller owes: a snapshot carries its domain's policy so
	// the redirect path needs no second lookup, so every cached alias under every
	// domain touched here is now wrong. The default is marked rather than sorted for,
	// since it is the row the settings surfaces read and the hostname they name.
	SetBotBlockingForEveryDomain(ctx context.Context, arg SetBotBlockingForEveryDomainParams) ([]SetBotBlockingForEveryDomainRow, error)
	// NULL clears it, which restores the 404 the root answered before anyone set
	// anything.
	SetDefaultDomainRootRedirect(ctx context.Context, rootRedirectUrl *string) (SetDefaultDomainRootRedirectRow, error)
	// Pins a workspace as where new sessions start, or clears the pin.
	//
	// NULL is a real value here and means "follow last-used", which is the default
	// the control offers. Clearing therefore needs no membership check; setting
	// needs the same one as above.
	SetDefaultWorkspaceForUser(ctx context.Context, arg SetDefaultWorkspaceForUserParams) (int64, error)
	// Where a verified hostname's own root sends a visitor (M40).
	//
	// The same column 00800 added for the instance default, addressed by id instead
	// of by `is_default`. A custom hostname is a bare domain somebody will type, and
	// answering 404 there is a choice its owner should get to make rather than
	// inherit from the instance.
	//
	// NULL clears it, restoring the 404.
	SetDomainRootRedirect(ctx context.Context, arg SetDomainRootRedirectParams) (Domain, error)
	// Runs only after the rollup returned without error, which is what makes
	// last_success_at mean what its name says. last_run_at cannot: RecordJobFailure
	// stamps it too, so a job failing on every tick would report itself fresh
	// forever and the staleness alert would never fire.
	SetJobWatermark(ctx context.Context, arg SetJobWatermarkParams) error
	// Remembers a selection, and refuses one the user is not entitled to.
	//
	// The membership check is in the statement rather than in a preceding SELECT so
	// there is no window between the two. Zero rows means "not yours or not there",
	// which the caller reports as not-found: a workspace id must not be probeable
	// for existence.
	SetLastWorkspaceForUser(ctx context.Context, arg SetLastWorkspaceForUserParams) (int64, error)
	SetPrimaryDestination(ctx context.Context, arg SetPrimaryDestinationParams) error
	// Stores the re-encoded image against one code.
	//
	// **One statement, so setting and replacing are the same operation and neither
	// has a gap in it.** A logo that replaced another leaves nothing behind to
	// collect: the previous value is overwritten by this write rather than deleted
	// by a second one, which is the property the storage decision was chosen for
	// (D134) and the reason "replacing removes the artefact it replaced" needs no
	// code of its own.
	//
	// The bytes are already bounded — the request body by http.MaxBytesReader, the
	// decode by qr.MaxDecodedLogoPixels, and this value by qr.MaxLogoStoredBytes,
	// which internal/qr enforces rather than assumes. Since D180, qr.MaxLogoPixels
	// bounds the *stored* artefact alone and refuses nothing: an image above it is
	// resampled down to it before it reaches this statement.
	SetQRCodeLogo(ctx context.Context, arg SetQRCodeLogoParams) (int64, error)
	// Moves one session, and only the session that asked.
	//
	// Scoped by user_id as well as id so a session id from elsewhere cannot be
	// repointed, and revoked sessions are excluded because moving one would be
	// writing to a credential that no longer authenticates.
	SetSessionWorkspace(ctx context.Context, arg SetSessionWorkspaceParams) (int64, error)
	// Instance-level settings: the answers an operator gives about the box rather
	// than about a tenant in it (M55). One row, guaranteed by 04300's primary key,
	// so every statement here is written against `id` and returns or touches
	// exactly one.
	//
	// **The column is nullable and NULL means unanswered** (D164). Three statements
	// rather than two because that third state has to be readable — the prompt an
	// upgraded instance gets at its first administrative sign-in is drawn from
	// exactly one fact, *has anybody answered this yet*, and there is nowhere else
	// to read it from. A `GetInstanceSettings` returning the whole row was written,
	// generated unused into the Querier interface, and removed: nothing renders
	// these settings, and a read of everything is the shape a settings API grows out
	// of before anything has asked for one.
	//
	// Two statements write the answer, and the difference between them is which
	// state they are allowed to leave. Setup may rewrite, because nothing has been
	// committed to until the instance is claimed; the principal answers once.
	// Record the operator's answer to the first-run prompt, at setup (D149).
	//
	// **Unconditional, which is deliberate and is the difference from
	// AnswerUpdateCheck below.** The instance is unclaimed — `SetUpdateCheckAtSetup`
	// has just counted the users to be sure of it — so an answer already sitting in
	// the row is a previous setup attempt whose `Register` failed, and the operator
	// retrying with the box unticked must be able to replace a yes with a no. A
	// conditional write here would make the first attempt's answer the permanent one.
	//
	// The row count is the check: the row is inserted by migration 04300, so zero
	// means the settings row is missing and the answer went nowhere. The setup path
	// reads it rather than assuming, because the failure it guards against is an
	// operator declining the update check and being checked on anyway.
	SetUpdateCheckEnabled(ctx context.Context, enabled bool) (int64, error)
	// Soft, unlike a folder. A domain is the namespace its links' aliases live in
	// and `links.domain_id` is NOT NULL with no cascade, so a hard delete is refused
	// by the database the moment one link exists; a soft delete keeps the row that
	// every historic click event and reserved alias still points at.
	SoftDeleteDomain(ctx context.Context, id uuid.UUID) (int64, error)
	// Soft delete with a purge deadline rather than an immediate DELETE. Restoring
	// a link someone deleted by accident is a common request, and the alias stays
	// reserved while the row exists.
	SoftDeleteLink(ctx context.Context, arg SoftDeleteLinkParams) (SoftDeleteLinkRow, error)
	// The deletion itself: the first writer `status` and `deleted_at` have ever had.
	//
	// `status = 'deleted'` and `deleted_at` are set together and only together. The
	// timestamp is what every query in this product filters on and what releases the
	// address through the partial unique index; the status is what a person reading
	// the row sees. Setting one without the other would make the two disagree about
	// the same fact, which is the state the CHECK constraint cannot catch.
	//
	// `password_hash` is **not** cleared here. Scrubbing is the erasure pass's, and
	// clearing it early would take the one field that makes a mistaken deletion
	// recoverable by an operator inside the sweep's window, while ending no access
	// that the session and key rows going in the same transaction have not already
	// ended.
	SoftDeleteUser(ctx context.Context, userID uuid.UUID) (int64, error)
	// Match a presented code and spend it, in one statement.
	//
	// Scoped by `user_id` as well as by the hash. The pending login already names the
	// account, and matching on the hash alone would make this table a global lookup —
	// correct today, because the hash index is unique, and one refactor away from a
	// code minted for one account opening another.
	//
	// `used_at IS NULL` is in the predicate for the reason the replay guard's
	// comparison is: single use has to be decided by the statement that spends it, or
	// two simultaneous presentations of the same code both pass their check.
	SpendMFARecoveryCode(ctx context.Context, arg SpendMFARecoveryCodeParams) (int64, error)
	// Batch write of last_used_at, from the coalescing tracker rather than from the
	// request path: authenticating a key must not cost a synchronous write.
	//
	// GREATEST guards against a late batch moving the timestamp backwards, which
	// two processes flushing out of order would otherwise do.
	TouchAPIKeys(ctx context.Context, arg TouchAPIKeysParams) error
	// Idle expiry is measured from last_seen_at. Updated at most once a minute by
	// the caller, because writing on every request would turn a read-mostly path
	// into a write on the hottest authenticated query.
	TouchSession(ctx context.Context, id uuid.UUID) error
	// Takes every link out of a deleted campaign.
	//
	// Run in the same transaction as DeleteCampaign. Without it the links keep an id
	// pointing at a row no query returns, which is a link filtered by a campaign
	// that is not in the campaign list — the invisible-rows failure 02400 describes
	// for folders, in the one place the schema does not prevent it.
	UnassignCampaignLinks(ctx context.Context, arg UnassignCampaignLinksParams) (int64, error)
	// The end of the grace window, and the only place serving stops on its own.
	//
	// `verified_at` is cleared, so the next ListVerifiedDomains does not return the
	// row, so every replica's host cache drops it and the hostname goes back to
	// ops-only 404. ssl_status goes with it: this instance will stop answering
	// Caddy's ask for the name, which is the other half of no longer serving it.
	//
	// The failing streak is *kept*. The page has to be able to say "this stopped
	// being served at 03:00 because it had been failing since yesterday", and
	// clearing the anchor here would throw away the only record of why.
	UnverifyDomain(ctx context.Context, arg UnverifyDomainParams) (Domain, error)
	//
	// Partial: a NULL parameter leaves its column alone, the shape every other
	// update in this schema has.
	//
	// **Re-arming is in this statement and not a second one.** A rule switched from
	// disabled to enabled has its watermark moved to the arming instant, so a rule
	// that was off for a month does not fire for a month of backlog the moment
	// somebody flips the switch. Switching one *off* leaves the watermark where it
	// is, because a disabled rule is not evaluated at all and the value is what a
	// reader is shown.
	//
	// The re-arm resets **both halves** of the watermark. The pair describes one
	// position in the match order, and a stale subject id beside a fresh instant
	// would describe a position that never existed — one that admits subjects tied
	// on the arming instant whose ids happen to sort above the old boundary. NULL is
	// the "instant fully spent" spelling, the same one creation uses.
	UpdateAutomationRule(ctx context.Context, arg UpdateAutomationRuleParams) (AutomationRule, error)
	// Partial update through COALESCE, like UpdateLink. The two schedule bounds are
	// three-valued through their own clear flags, because "leave the end date alone"
	// and "this campaign no longer ends" are different requests and one nullable
	// parameter cannot express both.
	UpdateCampaign(ctx context.Context, arg UpdateCampaignParams) (Campaign, error)
	// Has anybody answered the update-check question on this instance?
	//
	// The whole of what the prompt needs, and deliberately not the value: whether
	// the check is *on* is decided inside ClaimUpdateCheck, and a second reader of
	// that fact is a second place for it to be got wrong. This answers only *is the
	// question still open*, which is what decides whether an administrator is asked.
	UpdateCheckAnswered(ctx context.Context) (bool, error)
	// The trigger on destinations mirrors this into links.primary_url, so the hot
	// path never joins.
	//
	// Narrowed to the *primary* destination by M34, and the narrowing is the point
	// rather than tidying. Until routing rules existed a link had exactly one
	// destination row, so matching on link_id alone matched it; a rule target is a
	// second row on the same link, and this query would have rewritten every one of
	// them to the link's own URL the next time somebody edited the link. Every rule
	// on the link would silently start pointing at the same place, which is
	// indistinguishable from the rules having stopped working.
	//
	// Matched through links.primary_destination_id rather than through `position =
	// 0`, because that column is what the sync trigger keys on and what the rest of
	// the schema treats as the authority. Two definitions of "the primary" is how
	// they come to disagree.
	UpdateDestinationURL(ctx context.Context, arg UpdateDestinationURLParams) error
	// COALESCE with sqlc.narg gives partial update: a NULL argument leaves the
	// column alone, so PATCH semantics need no dynamic SQL.
	UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error)
	// Scoped by organization as well as id, so the authorization decision the
	// service made cannot be applied to a row in another tenant.
	UpdateMembershipRole(ctx context.Context, arg UpdateMembershipRoleParams) (int64, error)
	// Renames one code. The slug is untouched on purpose: it is printed, and a
	// rename that moved it would break every copy already in the world.
	UpdateQRCodeLabel(ctx context.Context, arg UpdateQRCodeLabelParams) (int64, error)
	// Partial update, same COALESCE-with-narg shape as UpdateLink. The destination
	// is not here: changing where a rule points is a write to its `destinations`
	// row, so that the URL, its host and the tier check that accepted it stay in
	// one place.
	UpdateRoutingRule(ctx context.Context, arg UpdateRoutingRuleParams) (RoutingRule, error)
	// Scoped to one destination id, unlike UpdateDestinationURL. A rule target and
	// the link's own destination are two rows on the same link now, and the wrong
	// one of these two queries would move both.
	UpdateRuleDestinationURL(ctx context.Context, arg UpdateRuleDestinationURLParams) error
	UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
	// The URL, the weight, or both. Scoped to one destination id for the reason
	// UpdateRuleDestinationURL is: a link's own destination and its arms are rows in
	// the same table on the same link.
	UpdateVariantDestination(ctx context.Context, arg UpdateVariantDestinationParams) error
	// Only the enabled flag, because that is the only thing on the rule row a
	// variant has. A weight is a write to the destination; a kind is not editable at
	// all, since changing one arm's kind would mix two kinds on a link and the
	// service refuses that outright.
	UpdateVariantRule(ctx context.Context, arg UpdateVariantRuleParams) (RoutingRule, error)
	//
	// Partial: a NULL parameter leaves its column alone. Same shape as every other
	// update in this schema, so "absent" and "empty" stay different — an empty
	// events array is a real request to subscribe to nothing.
	UpdateWebhook(ctx context.Context, arg UpdateWebhookParams) (UpdateWebhookRow, error)
	// Writes one entry from LINKCTRL_DESTINATION_BLOCKLIST at boot.
	//
	// ON CONFLICT DO UPDATE rather than DO NOTHING: an operator who moves a host
	// into their environment expects the environment to own it from then on, and a
	// row left claiming it came from a review would send M31 looking for a review
	// that never happened. created_at is left alone, because the entry is the same
	// entry it was before the restart.
	UpsertEnvBlockedDestination(ctx context.Context, arg UpsertEnvBlockedDestinationParams) error
	// One row per (link, slug), which qr_codes_link_slug_key (03700) is what makes
	// true. Without the unique index this is two concurrent inserts and a link with
	// two codes answering to one name.
	//
	// The label is not in the DO UPDATE list. This statement is how a *style* is
	// written, and a style write must not silently rename the code it is drawn for;
	// UpdateQRCodeLabel is the operation that renames one. **Nor is the logo**, for
	// the same reason and with more at stake: restyling a code must not throw away
	// the image somebody uploaded to it, and the insert branch leaves the column at
	// its NULL default because a code that has just come into being has no logo.
	// `is_default` is not in the DO UPDATE list either, and for the strongest of the
	// three reasons: which code an untagged scan resolves through is not something a
	// style write may move. ClearDefaultQRCode and MarkDefaultQRCode are the pair
	// that moves it, and they are the only pair that does.
	UpsertQRCode(ctx context.Context, arg UpsertQRCodeParams) (UpsertQRCodeRow, error)
}

type Queries

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

func New

func New(db DBTX) *Queries

func (*Queries) AbandonUnsendableMail added in v0.2.0

func (q *Queries) AbandonUnsendableMail(ctx context.Context, maxAgeDays int32) (int64, error)

Fails pending rows on an instance that has no relay to send them.

The outbox has one guard against unbounded growth — PurgeFinishedMail — and it takes only `status <> 'pending'`, which is correct while a mailer exists: Drain claims every pending row and moves it to sent or, after five attempts, failed, and the lease recovers rows a crash interrupted. So nothing stays pending, and nothing needs to.

Clearing SMTP_HOST on an instance that had one breaks that. The mailer becomes nil, so the drain does not run; the rows enqueued before the change stay pending; and the purge skips them by design. They are then invisible — nothing but CountPendingMail reads them — and permanent (F52).

Failing rather than deleting, so the record of what was attempted survives its retention window like every other finished row, and reaches PurgeFinishedMail by the normal path rather than by a second delete. The body goes for the same reason MarkMailFailed drops it: a message that will never be sent should not keep holding what it was going to say.

Bounded by the same window rather than run eagerly, because an operator who clears SMTP_HOST by mistake and puts it back the same afternoon should still get their queue delivered. Only the caller decides when this applies: it runs on the no-mailer path and nowhere else.

func (*Queries) AcceptMFAStep added in v0.3.0

func (q *Queries) AcceptMFAStep(ctx context.Context, arg AcceptMFAStepParams) (int64, error)

The replay guard, applied as a write rather than as a check.

`mfa_last_step < @step` is the whole mechanism. A code from a step already accepted updates no rows, and the caller reads zero as *refused*, so two requests presenting the same code race each other into the same statement and exactly one wins. Doing it as a read-then-write would leave a window in which both saw the old value, which for a replay guard is the only window that matters.

`mfa_last_step IS NULL` is the first acceptance after an enrolment that pre-seeded nothing, and is kept for completeness — `EnableUserMFA` stamps the enrolling step, so in practice the column is never NULL while a secret exists.

func (*Queries) AnswerUpdateCheck added in v0.3.0

func (q *Queries) AnswerUpdateCheck(ctx context.Context, enabled bool) (int64, error)

Record the answer given at the first administrative sign-in after an upgrade (D164).

**Conditional on the question still being open**, so this is a first answer and never a change of one. Two things fall out of that and both are the point: two browser tabs racing produce one answer and one no-op rather than last-write-wins, and the route cannot become the instance-settings page D161 refused to build — there is no second answer to give through it.

Row count 1 means this caller's answer is the one that landed. Zero means the question was already answered, by them or by somebody else holding `instance.admin`, and the caller's own read has already established that the row exists.

func (q *Queries) ArchiveLink(ctx context.Context, arg ArchiveLinkParams) (Link, error)

func (*Queries) ArchiveLinkByAutomation added in v0.2.0

func (q *Queries) ArchiveLinkByAutomation(ctx context.Context, arg ArchiveLinkByAutomationParams) (ArchiveLinkByAutomationRow, error)

The archive an automation performs. Idempotent: a link already archived comes back unchanged rather than erroring, so a rule whose window overlapped an interactive archive does not fail its whole firing over it.

Not `ArchiveLink`, and the difference is the missing actor. Every interactive archive is authorized against a signed-in identity in internal/link; this one is authorized by the rule, which was itself created by somebody holding `automation.write`. Scoped to the workspace in the statement, so a rule can only ever reach its own tenant's links.

**`expires_at` is not touched**, and that is load-bearing rather than incidental: moving it would make the link-expired trigger match this link again on the next run, which is the self-feeding cycle the vocabulary is arranged to make impossible.

func (*Queries) AttachTag

func (q *Queries) AttachTag(ctx context.Context, arg AttachTagParams) error

func (*Queries) CarryAPIKeyReachRevocations added in v0.3.0

func (q *Queries) CarryAPIKeyReachRevocations(ctx context.Context, arg CarryAPIKeyReachRevocationsParams) error

The predecessor's bars, carried onto its successor inside the rotation transaction.

**Without this a reach revocation is escapable by the credential it was aimed at.** A bar names an `api_key_id`; rotation mints a new id; nothing copied the rows. So the holder of a key an administrator had cut out of an organization sent `POST /api/v1/api-keys/rotate` — authenticated by the key's own token, with no session and no permission anywhere — and the successor resolved back into that organization and was told about it again. Driven through the product 2026-08-09, both halves: it acted there and it read there. That is the case the whole mechanism exists for, because the credential a reach revocation is aimed at is the one that is *not* in legitimate hands, and it is exactly the holder of that credential who has a reason to rotate.

**Both columns are copied rather than rewritten**, and that is the substance of the statement rather than a detail of it.

  • `revoked_at` is the predecessor's. It is when the reach was cut, and rotating does not move that moment. `now()` would date an administrator's act by the clock of whoever rotated, make an old bar read as this morning's, and hand the owner's key list (F178) a date that resets every time the credential is replaced — which is precisely the reading somebody evading the bar would want it to have.
  • `revoked_by` is the predecessor's. The bar is that administrator's statement and they are who answers for it; attributing it to the rotating actor would name a credential's holder as the author of a bar against itself. NULL is carried unchanged and is already a state the schema means: `revoked_by` is `ON DELETE SET NULL` exactly so a bar outlives the administrator's account.

**The two cases that copy nothing do so by data, not by a branch here.** A **pinned predecessor** has no rows to select — nothing writes a bar for a key whose organization is its whole reach, because cutting that reach and revoking the key are the same act (`revokeInOrganization` sends it to `revokePinnedKey`). And a **pinned successor** is excluded by the join, which reads the reach off the row `insertSuccessor` has just written rather than off a parameter that could disagree with it: an account-wide key may rotate into a pinned one, and copying bars onto that would leave rows no resolution path reads and put *cut out of Acme* on a key pinned to Beta. It would also break 04200's stated invariant that a pinned key never carries one.

Nothing is re-derived. Which organizations are barred is not recomputed from memberships or from anything else — the rows are the record, and a copy is the only operation that cannot disagree with them.

func (*Queries) ClaimAutomationRule added in v0.2.0

func (q *Queries) ClaimAutomationRule(ctx context.Context, arg ClaimAutomationRuleParams) (int64, error)

The compare-and-set that fires a rule. **This is the loop guard.**

The watermark is advanced *before* the actions run, and only if it is still exactly where the match query saw it. Two things follow, and both are deliberate:

  • A rule cannot fire twice for one subject. The window the next run reads starts after the subject that was just handled, so the match set that produced this firing can never be produced again.
  • A second replica that briefly believes it is the leader loses the race rather than duplicating the firing — the same reasoning D77 gives for claiming a webhook delivery with FOR UPDATE SKIP LOCKED under the advisory lock, and the same reason: an advisory lock is released the instant its holder dies, so a moment of overlap is possible and has to cost nothing.

The trade this direction makes is that a process killed between the claim and the actions loses that firing rather than repeating it. That is the right way round for an instruction that archives links and sends events: a missed notification is recoverable by looking, and a rule that archived the same links twice would be one nobody could trust to run unattended.

`IS NOT DISTINCT FROM` rather than `=`, so a rule whose watermark is NULL — the timestamp half only reachable by a row written outside this product, the subject half on every rule that has not fired since 03600 — compares correctly instead of never matching.

Both halves are set and both halves are compared. The pair is one position in the match order — the last subject a firing handled — and a claim that moved one half against a stale reading of the other would hand two racing replicas two different positions for the same firing.

func (*Queries) ClaimDueMail added in v0.2.0

func (q *Queries) ClaimDueMail(ctx context.Context, arg ClaimDueMailParams) ([]ClaimDueMailRow, error)

One batch of due mail, claimed rather than merely selected.

The UPDATE is what makes the claim: it spends the attempt and leases the row forward, in one statement, before anything is sent. Two consequences, and both are the point:

  • A process killed between claiming and sending leaves a row that comes back on its own when the lease expires, instead of one stuck pending.
  • A crash loop is bounded. Counting the attempt at send time would let a process that dies mid-send retry the same message forever.

FOR UPDATE SKIP LOCKED inside the subquery keeps two drainers from claiming the same row. Leadership already keeps a second replica out of this job, but leadership is an advisory lock released when its holder dies, so a moment of overlap is possible; skip-locked makes that moment cost nothing rather than send a message twice.

Ordered oldest first, so a backlog drains in the order it was queued.

func (*Queries) ClaimDueWebhookDeliveries added in v0.2.0

func (q *Queries) ClaimDueWebhookDeliveries(ctx context.Context, arg ClaimDueWebhookDeliveriesParams) ([]ClaimDueWebhookDeliveriesRow, error)

One batch of due deliveries, claimed rather than merely selected — the exact shape ClaimDueMail uses, and for the same two reasons:

  • The UPDATE spends the attempt and leases the row forward before anything is sent, so a process killed mid-delivery leaves a row that comes back on its own instead of one stuck pending.
  • A crash loop is bounded. Counting the attempt at send time would let a process that dies mid-send retry the same delivery forever.

FOR UPDATE SKIP LOCKED inside the subquery is the claim mechanism this milestone had to choose (see decisions.md). Leadership already keeps a second replica out of the job, but leadership is an advisory lock released when its holder dies, so a moment of overlap is possible; skip-locked makes that moment cost nothing rather than deliver the same event twice.

The webhook's URL and secret are joined in here rather than fetched per row: the drainer needs both for every claimed delivery, and N+1 round trips to assemble a batch of network calls is the wrong shape.

func (*Queries) ClaimUpdateCheck added in v0.3.0

func (q *Queries) ClaimUpdateCheck(ctx context.Context, arg ClaimUpdateCheckParams) (int64, error)

Take the day's update check, if it is available to take.

**The daily bound is this statement, not a ticker.** One UPDATE decides whether the check may run and records that it did, so the bound is a property of the instance rather than of one process's uptime: a replica restarted every ten minutes reads a row that says the check already happened and declines, where a bare timer would ask GitHub on every boot.

It writes the timestamp *before* the request rather than after it, which is what makes a failure cost one attempt instead of one per tick. The milestone forbids a retry storm and this is where that is enforced; a check that fails waits out the same day a check that succeeded does.

**`IS TRUE` rather than a bare test, because the column has three states** (D164). A bare `AND update_check_enabled` would already decline on NULL — unknown is not true — so the behaviour is the same and the spelling is not: *off while unanswered* is a decision this statement enforces, and a reader should not have to recover it from SQL's three-valued logic to be sure it was meant.

Row count 1 means the caller holds the check. Zero means the operator turned it off, or has not been asked yet, or somebody has already run it today, and the caller does not need to know which — all three are "do nothing".

func (*Queries) ClearDefaultQRCode added in v0.3.0

func (q *Queries) ClearDefaultQRCode(ctx context.Context, arg ClearDefaultQRCodeParams) (int64, error)

Moving the flag an untagged scan resolves through takes two statements and one transaction (M50's reopening, D183).

**Two rather than one, and the reason is the index rather than taste.** `UPDATE … SET is_default = (id = $3)` over the whole link reads as the obvious single statement, and `qr_codes_link_default_key` is a plain unique index, which Postgres checks as each row version is written rather than at the end of the statement. Such an update collides with itself whenever the scan reaches the incoming default before the outgoing one — the same failure `UPDATE t SET n = n + 1` has on a unique column. A partial index cannot be declared DEFERRABLE, because only a constraint can and a constraint cannot be partial, so the ordering is made explicit instead: clear, then set, inside the transaction the service opens.

The window between them holds a link with no default at all. It is invisible: the transaction has not committed, so no reader outside it sees either write, and inside it the only reader is the second statement. The first half. Takes the flag off whichever row holds it, or off nothing.

func (*Queries) ClearOrphanedQRCodeLogos added in v0.3.0

func (q *Queries) ClearOrphanedQRCodeLogos(ctx context.Context, batchSize int32) (int64, error)

The orphan sweep, run hourly by the maintenance pass.

**What is orphaned under a column, and what is not.** Removing a code, a workspace or an organization takes its logos by cascade, and replacing one is the single UPDATE above, so none of those can leave bytes behind. Deleting a *link* can, and does: a link is soft-deleted with a purge deadline, so its `qr_codes` rows survive the whole trash window while every read in this file filters them out with `l.deleted_at IS NULL`. Those bytes are unreachable through the product — the endpoint that would clear them answers 404 for a deleted link — and they sit in the row and in every `pg_dump` until the purge fires, which for a large backlog is several hourly runs away and for a row the purge skips is longer still.

This is what makes m50.5.md's claim *deleting the link removes its artefacts* true rather than merely intended. The row itself is left alone: the trash window exists so a link can be brought back by hand, and the artefact is the thing deletion was asked to remove.

Idempotent by construction — a second run matches nothing, because `logo IS NOT NULL` is the predicate. Bounded like every other pass in that job, and SKIP LOCKED so it can never block, or be blocked by, a concurrent write to the same code.

func (q *Queries) ClearQRCodeLogo(ctx context.Context, arg ClearQRCodeLogoParams) (int64, error)

Removes the image, and the row stays: a code without a logo is a code.

NULL rather than an empty bytea, because the schema has one spelling for "no logo" and two would disagree the first time somebody wrote a zero-length one.

**The style goes with it, in this statement rather than a second one** (M50.6's second reopening). The upload forces error correction to H and the removal puts it back, which is a style write — and a style write of its own would be an upsert, so a `DELETE` landing between the two would find no row to conflict with and **insert a fresh code**, slug and all. One statement keyed on the id cannot do that: a row that is gone updates nothing. The caller passes the style it read, unchanged, for a code that had no logo to begin with.

func (*Queries) ConsumeClickBudget added in v0.2.0

func (q *Queries) ConsumeClickBudget(ctx context.Context, arg ConsumeClickBudgetParams) (ConsumeClickBudgetRow, error)

Spend one click of a one-time or max-click link's durable budget.

**One statement, and that is the whole of the concurrency argument.** Two requests for the last click of a one-time link arrive at the same instant on different replicas; both reach here; Postgres serialises them on the row lock the ON CONFLICT path takes, so the second one re-evaluates its WHERE against the first one's committed value and matches nothing. A read-then-write in Go, or even a SELECT ... FOR UPDATE followed by an UPDATE, would need a transaction the caller could forget to open; there is no such transaction to forget here because the statement is the transaction.

The insert races too: two requests for the *first* click of the same link both try to INSERT, one wins, the loser takes the DO UPDATE branch and is evaluated against the winner's row. That is why the limit test lives in the conflict clause rather than only in the VALUES.

Returns no row when the budget is spent, which the caller reads as 410. That is deliberately the same shape as "no such link": the caller has a snapshot already and does not need this query to tell it the link exists.

click_limit is 1 for a one-time link, max_clicks otherwise, the smaller of the two when both are set. A limit below one can never match, which is correct: a link nobody may follow.

func (*Queries) ConsumeMFAPendingLogin added in v0.3.0

func (q *Queries) ConsumeMFAPendingLogin(ctx context.Context, id uuid.UUID) (int64, error)

Spend it. Single use, decided by the statement rather than by the caller, for the third time in this file and for the same reason.

func (*Queries) ConsumePasswordResets added in v0.3.0

func (q *Queries) ConsumePasswordResets(ctx context.Context, userID uuid.UUID) (int64, error)

Spends every unconsumed token for one account.

**One statement, called from both ends of the flow**, because the two needs are the same statement and writing it twice would be two places for the predicate to drift.

Requesting a reset calls it to supersede whatever was outstanding, so a fresh request takes the slot and the previous link stops working at the same moment — the shape DeleteOutstandingRegistration has for registrations, except consumed rather than deleted: a superseded reset is evidence somebody asked to recover this account twice, and the purge is what removes it later.

Completing a reset calls it to spend the token just used *and its siblings*, because a recovery that leaves a second live token behind has recovered nothing: whoever else requested one — including whoever the person is recovering from — would still hold a working link to the account whose password just changed.

func (*Queries) ConsumePendingRegistration added in v0.2.0

func (q *Queries) ConsumePendingRegistration(ctx context.Context, id uuid.UUID) (int64, error)

Spends a registration. Conditional on it still being unspent, so this could not succeed twice even without the lock above; zero rows rolls the transaction back.

func (*Queries) CountAutomationRules added in v0.2.0

func (q *Queries) CountAutomationRules(ctx context.Context, workspaceID uuid.UUID) (int64, error)

func (*Queries) CountCampaigns added in v0.2.0

func (q *Queries) CountCampaigns(ctx context.Context, workspaceID uuid.UUID) (int64, error)

func (*Queries) CountClickEvents

func (q *Queries) CountClickEvents(ctx context.Context, workspaceID uuid.UUID) (int64, error)

func (*Queries) CountDestinationWebhooks added in v0.2.0

func (q *Queries) CountDestinationWebhooks(ctx context.Context, arg CountDestinationWebhooksParams) (int64, error)

How many of this workspace's registrations actually receive a destination somebody typed. The `/feeds` disclosure is built on it (M45, F135).

**The predicate is the fan-out's predicate, deliberately.** EnqueueWebhookDeliveries below queues a row for `w.enabled AND event = ANY(w.events)`, so asking the same two conditions with the destination-carrying events as the set is asking *would anything have been queued* rather than guessing at it. A disclosure built on a looser test would warn about a registration that receives nothing; one built on a tighter test would reassure a workspace whose URLs are being posted somewhere.

The event names come from the caller (domain.WebhookDestinationEvents) rather than being written out here, because which payloads carry a destination is a fact about internal/link's payload builders and must not be restated in a second place that can drift from them.

Cost: the same partial index the fan-out uses, `webhooks_workspace_idx ... WHERE enabled` (00600), over at most MaxWebhooksPerWorkspace rows. It is read on a dashboard page and on GET /api/v1/feeds, neither of which is the redirect path.

func (*Queries) CountFolders added in v0.2.0

func (q *Queries) CountFolders(ctx context.Context, workspaceID uuid.UUID) (int64, error)
func (q *Queries) CountLinks(ctx context.Context, arg CountLinksParams) (int64, error)

Only issued when the caller explicitly asks for a total, because counting costs a scan the common page load should not pay for.

func (*Queries) CountLinksOnDomain added in v0.2.0

func (q *Queries) CountLinksOnDomain(ctx context.Context, domainID uuid.UUID) (int64, error)

What deletion is refused for. Zero on every registered hostname today, because nothing serves one and links are created on the default domain — the guard is here so that it is already true when M40 makes it reachable.

func (*Queries) CountMembershipsForEmail added in v0.2.0

func (q *Queries) CountMembershipsForEmail(ctx context.Context, arg CountMembershipsForEmailParams) (int64, error)

Whether the person at this address is already in this organization.

Asked at creation, where the actor holds members.write on the organization and could read its member list anyway, so answering it discloses nothing they could not already see. Redemption asks the same question of a user id it has already resolved, and answers it with the same generic refusal as every other failure.

func (*Queries) CountMembershipsForUser added in v0.2.0

func (q *Queries) CountMembershipsForUser(ctx context.Context, arg CountMembershipsForUserParams) (int64, error)

func (*Queries) CountNotificationsAboutVersion added in v0.3.0

func (q *Queries) CountNotificationsAboutVersion(ctx context.Context, arg CountNotificationsAboutVersionParams) (int64, error)

The other re-notify guard, and it is keyed on the thing rather than on the clock (M55).

CountRecentNotificationsOfKind above suppresses a warning that is *still true* — the audit log is still too big — so it asks "was this said lately". A release is a different shape: the answer is not that it was said lately, it is that this exact version has already been reported and reporting it again says nothing new. So the version is the key, and there is no window: an operator who was told about 0.4.0 a year ago is not told again, and 0.5.0 is a new fact that arrives once.

Reading `data->>'version'` rather than a column of its own is deliberate. The notification is the record that the operator was told; a column beside it would be a second place for the same fact, and the two would disagree the first time one write succeeded and the other did not.

func (*Queries) CountOpenDestinationDisputes added in v0.2.0

func (q *Queries) CountOpenDestinationDisputes(ctx context.Context) (int64, error)

What the queue's heading says there is to do. Served by the partial unique index, whose predicate this matches exactly.

func (q *Queries) CountOrganizationLinks(ctx context.Context, organizationID uuid.UUID) (int64, error)

What D37 refuses an organization deletion on, and it is deliberately the same shape as CountWorkspaceLinks one level up.

Archived links count, soft-deleted ones do not — the reasoning is D32's and is written out there. What is new here is *why the org level asks the question at all*: cascading through these links would make D32 bypassable by deleting one level up, which turns a rule into a speed bump. So the organization refuses on exactly the rows its workspaces would refuse on.

func (*Queries) CountOrganizationWorkspaces added in v0.2.0

func (q *Queries) CountOrganizationWorkspaces(ctx context.Context, organizationID uuid.UUID) (int64, error)

Whether this is the organization's last workspace.

Deleting it would leave every member of the organization resolving into no workspace at all, which `ResolveWorkspaceForUser` reports as a broken instance rather than as an empty state — so the account could not authenticate. Guarded for the same reason the last owner is.

func (*Queries) CountPendingMail added in v0.2.0

func (q *Queries) CountPendingMail(ctx context.Context) (int64, error)

func (*Queries) CountPendingWebhookDeliveries added in v0.2.0

func (q *Queries) CountPendingWebhookDeliveries(ctx context.Context) (int64, error)

func (*Queries) CountQRCodes added in v0.3.0

func (q *Queries) CountQRCodes(ctx context.Context, arg CountQRCodesParams) (int64, error)

What domain.MaxQRCodesPerLink is checked against, the way campaign creation checks CountCampaigns.

func (*Queries) CountRecentNotificationsOfKind added in v0.2.0

func (q *Queries) CountRecentNotificationsOfKind(ctx context.Context, arg CountRecentNotificationsOfKindParams) (int64, error)

The re-notify guard. A threshold that is still crossed is still crossed on the next run an hour later, and a notification per hour forever is how an inbox becomes something people stop reading — which would cost exactly the warning D5 depends on.

func (*Queries) CountRoutingRules added in v0.2.0

func (q *Queries) CountRoutingRules(ctx context.Context, linkID uuid.UUID) (int64, error)

Read before an insert, to enforce the per-link ceiling. Counts every kind, not only 'match': the ceiling exists because the whole list travels inside the cached snapshot and is walked in order on the redirect path, and M36's rows will be in that list too.

func (*Queries) CountUnreadNotifications added in v0.2.0

func (q *Queries) CountUnreadNotifications(ctx context.Context, arg CountUnreadNotificationsParams) (int64, error)

Served by notifications_user_unread_idx, the partial index the table already ships with: the WHERE clause here has to match the index's predicate exactly or this becomes a sequential scan on every page render in the dashboard.

**Both halves of the workspace predicate are load-bearing** (D102, F105). `workspace_id = @workspace_id` alone hides every organization-level notification, because disputes and audit-growth write NULL — the reader would lose exactly the notifications that are not about any one workspace. And the clause has to be identical here and in ListUnreadNotificationPreview below, or the badge and the list it previews disagree while one of them stops using the index.

func (*Queries) CountUnusedMFARecoveryCodes added in v0.3.0

func (q *Queries) CountUnusedMFARecoveryCodes(ctx context.Context, userID uuid.UUID) (int64, error)

What the account page shows. A number somebody acts on: three left is a prompt to regenerate, and zero with a lost phone is a conversation with the operator.

func (*Queries) CountUserMemberships added in v0.2.0

func (q *Queries) CountUserMemberships(ctx context.Context, userID uuid.UUID) (int64, error)

Whether an account belongs to anything at all.

Read by exactly one caller: the first-organization path (D36). An account with no membership holds no role and therefore no `orgs.create`, so without this the prompt to create an organization would lead somewhere it is refused. This is a check on **present state** — how many memberships exist right now — and not on how the account was made, which is the distinction D16 was drawing when it made the permission a grant rather than a provenance test.

func (*Queries) CountUsers

func (q *Queries) CountUsers(ctx context.Context) (int64, error)

Users, sessions and tenancy provisioning. Drives the first-run setup flow: /setup exists only while this is zero.

func (*Queries) CountUsersByEmail added in v0.2.0

func (q *Queries) CountUsersByEmail(ctx context.Context, email string) (int64, error)

Whether an address already has an account, for the signup form.

Counted rather than selected: the caller needs the answer and nothing else, and returning the row would put somebody else's name and hash in a variable that only ever gets compared against zero.

func (*Queries) CountWebhooks added in v0.2.0

func (q *Queries) CountWebhooks(ctx context.Context, workspaceID uuid.UUID) (int64, error)

func (*Queries) CountWorkspaceDomains added in v0.2.0

func (q *Queries) CountWorkspaceDomains(ctx context.Context, workspaceID *uuid.UUID) (int64, error)

How many hostnames this workspace has registered, which is what the per-workspace cap is applied to (M40, reopened).

Every undeleted row, verified or not, because what the cap bounds is the work a registration creates: one outbound DNS lookup per hostname per pass, against a nameserver the registrant chooses. An unverified hostname costs exactly the same lookup as a verified one, so counting only the verified ones would bound the wrong number.

func (q *Queries) CountWorkspaceLinks(ctx context.Context, workspaceID uuid.UUID) (int64, error)

What D32 refuses a workspace deletion on.

Soft-deleted links are excluded on purpose. `links`, `tags` and `folders` all cascade from `workspaces`, so the guard is in front of a real cascade — but a link the owner already deleted is one they cannot delete again, and counting it would leave the workspace undeletable until the purge job ran, with nothing the person could do about it. Archived links **are** counted: an archived link keeps its alias and its click history, so cascading it away would be silent data loss dressed as tidying up.

func (*Queries) CreateAPIKey

func (q *Queries) CreateAPIKey(ctx context.Context, arg CreateAPIKeyParams) (ApiKey, error)

API keys and the permission vocabulary their scopes are drawn from.

func (*Queries) CreateAutomationRule added in v0.2.0

func (q *Queries) CreateAutomationRule(ctx context.Context, arg CreateAutomationRuleParams) (AutomationRule, error)

Automation rules and their evaluation (M43).

Two halves that never meet in one statement, the shape webhooks.sql already has. The rule half is workspace-scoped and reached from the dashboard and the API; the evaluation half is reached only by the scheduler and deliberately carries no workspace parameter in its first query — a run that filtered by tenant would evaluate in tenant order, and the fairness this needs is least-recently-looked-at first across the instance.

**`last_fired_at` is a watermark, not a diagnostic — and it is half of one.** Every match query below orders by (event time, id) and takes the window that opens strictly after the pair `(@after, @after_subject)` and closes at `@until`. The pair, not the instant alone: a capped fetch can stop part-way through subjects sharing one timestamp, and a lower bound that carries only the timestamp cannot re-enter that tie group — it either skips the tied remainder forever (strict `>`) or re-fires what was already handled (`>=`). `last_fired_subject_id` (03600) is the id half. What the watermark is *for* is unchanged: a subject is matched once, the watermark moves past it, and no later run can see it again. Removing the advance turns every rule into a runaway that fires on the same subject on every tick.

**`last_checked_at` is the scheduler's cursor, and it is a different fact.** When a rule was last *looked at*, whether or not it fired. It orders the due query and bounds no window; the watermark bounds every window and orders nothing. Ordering on the watermark is what F83 was — it moves only when a rule fires, so an idle rule held the head of the queue permanently and the hundred-and-first enabled rule on an instance was never evaluated at all (03100).

`last_fired_at` is set at creation rather than left NULL. A NULL watermark on a rule created today would mean "every link that ever expired", so the first run of a brand-new rule would fire for the entire history of the workspace.

`last_checked_at` is left NULL and travels back with the row, as it does in every statement below. NULL sorts first in the due query, so a rule somebody just wrote is looked at on the next tick instead of waiting for its turn — and carrying the column in the projection is what keeps these four statements returning the table's own row type rather than four near-identical structs.

`last_fired_subject_id` is left NULL for the same projection reason and one of its own: a rule that has never fired has no boundary subject, and NULL is read by the evaluator as "the arming instant is fully spent" — the same strict `>` a timestamp-only watermark meant.

func (*Queries) CreateCampaign added in v0.2.0

func (q *Queries) CreateCampaign(ctx context.Context, arg CreateCampaignParams) (Campaign, error)

Campaigns and QR codes (M41).

Both tables were created dormant by 00600 and woken by 02700. They share this file because they share a milestone and neither is large enough to be worth its own; nothing else connects them.

**A campaign is soft-deleted and a QR style is not.** The asymmetry is deliberate. A campaign names a body of work whose links keep their history after it ends, and `links.campaign_id` is ON DELETE SET NULL, so a hard delete would unlabel every link the moment somebody tidied up a finished campaign — exactly the failure the folder migration (02400) argues against. `deleted_at` keeps the row, and every statement here filters on it. A QR style is a rendering preference with nothing to restore: deleting it returns the link's code to the default style, which is the state every link starts in.

func (*Queries) CreateDestination

func (q *Queries) CreateDestination(ctx context.Context, arg CreateDestinationParams) (Destination, error)

func (*Queries) CreateDomain added in v0.2.0

func (q *Queries) CreateDomain(ctx context.Context, arg CreateDomainParams) (Domain, error)

Domain ownership and registration (M39).

The settings queries for the instance default live in links.sql, where they were written when there was exactly one domain. These are the ones that exist because there can now be more than one, and every statement here reads or writes the *ownership* columns rather than the serving ones — nothing on a registered hostname is served until M40 verifies it.

Ownership is never decided in SQL. Every write below is by id, and link.Service reads the row first and judges the actor against it, so the refusal is a sentence naming whose domain it is rather than a statement that silently affected no rows. ListDomains is the exception, and it is a read: it is scoped by the actor's organization and workspace because a list is only ever the caller's own. Registered, and deliberately unverified: verified_at stays NULL and ssl_status stays at its 'none' default. is_default is never set here — there is one instance default and 00700 seeded it.

The challenge token is minted here (M40) rather than lazily on the first verification attempt, so the page that tells somebody which DNS record to publish can do it the moment they register — the alternative is a page that asks them to come back. **The bot policy is inherited from the instance default (F89).** It is instance-wide, and a hostname registered after the operator turned blocking on would otherwise start at the column defaults and reopen the hole propagating the setting closes. Read from the default row rather than passed in, so there is no argument a caller can get wrong and no second place the two settings can disagree. Both subqueries read the same row; if the seeded default were ever missing they COALESCE to false together, which is a state the CHECK accepts.

func (*Queries) CreateFolder added in v0.2.0

func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (Folder, error)

Folders (M38).

**There is no recursive SQL here, and that is a decision rather than an omission.** A folder tree is naturally a WITH RECURSIVE walk, and the walk was written first; it was replaced by ListFolders, which reads the workspace's folders flat and lets internal/link assemble the tree in Go. Three reasons, in the order they matter:

  • The interesting rules are not "who are my children". They are "may this move happen" — a folder may never become its own descendant — and "how deep would the result be". Both are walks over the same set, and running them as three more recursive CTEs would put the milestone's two named failure modes in three places where only integration tests can reach them. In Go they are one function with a unit test that does not need Postgres.
  • The set is small by construction. The depth cap is domain.MaxFolderDepth, the sibling-name rule stops a tree fanning out by accident, and this is a structure a person curates by hand.
  • A recursive CTE over a table with no cycle constraint runs forever if the data ever holds one. The Go walk carries a visited set and stops.

Deleting a folder is a real DELETE (see migration 02400): `parent_id ON DELETE CASCADE` takes the subtree and `links.folder_id ON DELETE SET NULL` unfiles every link in any part of it. Every statement below still filters on `deleted_at IS NULL`, so the partial indexes serve them and a later decision to soft-delete does not silently resurrect rows.

func (*Queries) CreateInvitation added in v0.2.0

func (q *Queries) CreateInvitation(ctx context.Context, arg CreateInvitationParams) (Invitation, error)

Invitations: issuing, listing, revoking and redeeming (M27).

func (q *Queries) CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error)

Links, destinations and tags.

func (*Queries) CreateMFAPendingLogin added in v0.3.0

func (q *Queries) CreateMFAPendingLogin(ctx context.Context, arg CreateMFAPendingLoginParams) (MfaPendingLogin, error)

The credential the browser holds between a right password and a session.

Returned in full so the caller can assert the expiry it asked for rather than recompute it from its own clock — the TTL m53.md wants a test to hold is the one the database wrote.

func (*Queries) CreateMembership

func (q *Queries) CreateMembership(ctx context.Context, arg CreateMembershipParams) (Membership, error)

func (*Queries) CreateOrganization

func (q *Queries) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)

func (*Queries) CreatePasswordReset added in v0.3.0

func (q *Queries) CreatePasswordReset(ctx context.Context, arg CreatePasswordResetParams) (PasswordReset, error)

Account recovery: the reset tokens a forgotten password is repaired with (M51). The account's own row is written through query/auth.sql's UpdateUserPassword, so there is one password-writing statement in the product and not two.

func (*Queries) CreatePendingRegistration added in v0.2.0

func (q *Queries) CreatePendingRegistration(ctx context.Context, arg CreatePendingRegistrationParams) (PendingRegistration, error)

Self-serve signup: the registrations waiting on an address to be proven (M29). The mode itself is `LINKCTRL_SIGNUP_MODE` and is never read from the database (D38), so nothing here answers what the instance admits.

func (*Queries) CreateRoutingRule added in v0.2.0

func (q *Queries) CreateRoutingRule(ctx context.Context, arg CreateRoutingRuleParams) (RoutingRule, error)

Routing rules (M34).

Every query here filters on kind = 'match'. That is not defensive coding: the column's CHECK also permits weighted, sequential and fallback, which are M36's, and a query that read every kind would start behaving differently the day those rows first exist — silently, on the redirect path. The filter means M36 has to write its own reads, which is the correct amount of work for a milestone that adds a new evaluation model.

Rule *targets* are ordinary `destinations` rows. A rule therefore costs two writes and the destination is what carries the URL, its host, and the M30 tier check the service applied before either row existed.

func (*Queries) CreateRuleDestination added in v0.2.0

func (q *Queries) CreateRuleDestination(ctx context.Context, arg CreateRuleDestinationParams) (Destination, error)

A rule's target. Position is above zero so it can never be mistaken for the link's own destination, which Phase 1 put at position 0 and which `links.primary_destination_id` points at.

func (*Queries) CreateSalt

func (q *Queries) CreateSalt(ctx context.Context, arg CreateSaltParams) ([]byte, error)

ON CONFLICT DO NOTHING returns no row when another replica inserted first, which the caller detects and re-reads. Two replicas using different salts for the same day would split every visitor in two.

func (*Queries) CreateSession

func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)

workspace_id is written at sign-in rather than left for the first switch, so a session says where it is from its first row. Resolution would answer the same either way — a NULL simply falls through to the user's preference — but a switcher that only takes effect after the first switch is a switcher whose state is unreadable until somebody uses it.

func (*Queries) CreateTag

func (q *Queries) CreateTag(ctx context.Context, arg CreateTagParams) (Tag, error)

--- tags -------------------------------------------------------------------

func (*Queries) CreateUser

func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)

func (*Queries) CreateVariantDestination added in v0.2.0

func (q *Queries) CreateVariantDestination(ctx context.Context, arg CreateVariantDestinationParams) (Destination, error)

A variant's target, with its weight. The same row CreateRuleDestination writes, plus the one column that makes it an arm rather than a target: 00300 has carried `weight` since Phase 1 with a comment naming this milestone.

func (*Queries) CreateVariantRule added in v0.2.0

func (q *Queries) CreateVariantRule(ctx context.Context, arg CreateVariantRuleParams) (RoutingRule, error)

--- split testing (M36) -----------------------------------------------------

A variant is a rule row of kind weighted, sequential or fallback, pointing at its own `destinations` row exactly as a match rule's target does. So the writes below are the same two writes, and the M30 tier check the service applied before either row existed is the same check.

Every query here excludes `match` for the reason every query above requires it: the two management surfaces address disjoint sets of rows, so a rule id handed to the wrong endpoint finds nothing rather than editing a rule of a kind the caller was not looking at. Priority is fixed at the column default and never read for a variant: arms are chosen, not matched in order, and a priority on one would be a number the dashboard shows and nothing obeys. Creation order is what orders a rotation, which is what ListVariantRules sorts by.

func (*Queries) CreateWebhook added in v0.2.0

func (q *Queries) CreateWebhook(ctx context.Context, arg CreateWebhookParams) (CreateWebhookRow, error)

Webhooks and their delivery queue (M42).

Two halves that never meet in one statement. The registration half is workspace-scoped and reached from the dashboard and the API; the delivery half is scoped to nothing but time and is reached only by the scheduler. Every query below belongs to exactly one of them, and the delivery half deliberately carries no workspace parameter — a drainer that filtered by tenant would deliver in tenant order, and the queue is fair or it is a queue somebody's backlog can starve.

func (*Queries) CreateWorkspace

func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, error)

func (*Queries) DecideDestinationDispute added in v0.2.0

func (q *Queries) DecideDestinationDispute(ctx context.Context, arg DecideDestinationDisputeParams) (DestinationDispute, error)

Records a decision, and only on a dispute nobody has decided yet.

The `status = 'open'` predicate is the concurrency control: two owners clicking allow and uphold on the same row produce one decision and one no-rows, rather than a last-writer-wins that leaves the audit record and the blocklist disagreeing about what happened.

func (*Queries) DeleteAccountDependents added in v0.3.0

func (q *Queries) DeleteAccountDependents(ctx context.Context, accountID uuid.UUID) (DeleteAccountDependentsRow, error)

Everything hanging off the account that must not outlive it, removed in one statement and counted.

**Written out because a soft delete fires no foreign key.** All eight tables below declare `ON DELETE CASCADE` against `users`, and every one of those clauses triggers on `DELETE`; the account row is kept — that is what `anonymized_at` marks and what the partial `users_email_key` is shaped for — so the cascade never runs and these statements are what stands in for it.

Four of them are the tables M52 enumerates: `memberships`, `sessions`, `api_keys`, `notifications`. Four more are here because leaving them would falsify a claim the schema already makes:

  • `password_resets`, whose own comment (03900) says *"there is no route by which a reset for a deleted account could still be consumed, because the row is gone with the account"*. Under a soft delete the row is not gone, and it is the one credential in this schema that sets a password.
  • `instance_grants`, whose own comment (03400) says a grant naming a user who does not exist *"is not a record worth keeping, it is a permission nobody can hold"*. The instance principal cannot reach this statement at all — deleting it is refused — but a delegated dispute reviewer can.
  • `mfa_recovery_codes` and `mfa_pending_logins` (04100), added by M53 and added *by* M53 rather than deferred, because M53 is what creates them: a recovery code is a standing credential that admits somebody to an account with no password, and a pending login is one that mints a session. Both are the `password_resets` defect in a new table, and shipping the tables without the statements would have reintroduced it in the same phase that closed it.

The counts come back so the caller can log what went, and so a test can assert the statement reached each table rather than assert it did not error.

func (*Queries) DeleteAutomationRule added in v0.2.0

func (q *Queries) DeleteAutomationRule(ctx context.Context, arg DeleteAutomationRuleParams) (int64, error)

func (*Queries) DeleteBlockedDestination added in v0.2.0

func (q *Queries) DeleteBlockedDestination(ctx context.Context, host string) (int64, error)

Removes one host from the low-confidence runtime list.

The only deletion in this program that is not a reconciliation, and the only one an `allow` decision performs. Scoped to an exact host — the row the dispute recorded when it was filed and the queue displayed on the button — so a decision about 'login.evil.example' cannot take 'evil.example' off the list by accident. It can take it off deliberately, and routinely does: 'evil.example' is what refused 'login.evil.example', and lifting anything else would leave the destination refused. What 03300 changed is that the owner is now told which of the two they are deciding about, and that the answer cannot move between the filing and the click.

It cannot reach the other two tiers, and there is nothing to scope against them: the embedded list is a compiled file and the unappealable tier has no row anywhere. That is the structural half of "decisions act only on the runtime low-confidence list".

func (*Queries) DeleteCampaign added in v0.2.0

func (q *Queries) DeleteCampaign(ctx context.Context, arg DeleteCampaignParams) (int64, error)

Soft. The links keep their history; `links.campaign_id` is cleared by the statement below rather than by a cascade, because a cascade would fire on a hard delete this statement never performs.

func (*Queries) DeleteExpiredSessions

func (q *Queries) DeleteExpiredSessions(ctx context.Context) (int64, error)

Reaper. Revoked rows are kept briefly so "sign out everywhere" is visible in the session list before it disappears.

func (*Queries) DeleteFolder added in v0.2.0

func (q *Queries) DeleteFolder(ctx context.Context, arg DeleteFolderParams) (int64, error)

A real DELETE. The two foreign keys 00300 wrote are what make it safe: descendants cascade, and every link in the subtree has its folder_id set to NULL. Nothing here touches `links`, and nothing here may: the moment this statement grows a link delete, deleting a container starts deleting content.

func (*Queries) DeleteMFAPendingLoginsFor added in v0.3.0

func (q *Queries) DeleteMFAPendingLoginsFor(ctx context.Context, userID uuid.UUID) (int64, error)

Every outstanding pending login for an account.

Two callers. A fresh password post supersedes whatever was outstanding, so there is never more than one live prompt per account and somebody who abandoned a tab is not sharing their window with it. And disabling the second factor takes them all, because a prompt that outlives the factor it was prompting for is a credential with nothing left to check.

func (*Queries) DeleteMFARecoveryCodes added in v0.3.0

func (q *Queries) DeleteMFARecoveryCodes(ctx context.Context, userID uuid.UUID) (int64, error)

Every code for an account, spent ones included.

Both callers want exactly this. Regenerating voids the previous set in full, so keeping the spent rows would leave a count of leftovers from a set that no longer opens anything. Disabling removes the account's last credential of this kind, and m53.md names it in the same breath as clearing the secret.

func (*Queries) DeleteMembership added in v0.2.0

func (q *Queries) DeleteMembership(ctx context.Context, arg DeleteMembershipParams) (int64, error)

func (*Queries) DeleteOrganization added in v0.2.0

func (q *Queries) DeleteOrganization(ctx context.Context, id uuid.UUID) (int64, error)

A real delete, not a soft one, and everything in 00200/00300/00500/01200 that references it cascades: workspaces and everything under them, memberships, invitations, API keys, and any custom domain.

Two things deliberately do not. `audit_logs.organization_id` carries no foreign key, so the trail this organization wrote survives the row it describes — a tenancy teardown that erased its own record is the one shape an audit log must not have. And the instance default domain has `organization_id IS NULL`, so it and the `reserved_aliases` rows keyed to it are untouched.

func (*Queries) DeleteOrganizationRollups added in v0.2.0

func (q *Queries) DeleteOrganizationRollups(ctx context.Context, organizationID uuid.UUID) (int64, error)

The three analytics rollups belonging to an organization's workspaces.

They carry `workspace_id` with no foreign key — a deliberate choice recorded at 00400, so a rollup job never blocks on a tenancy write and a partition drop never has to consider them — and the cost of that choice is that nothing cascades them. `link_click_daily`, `link_dimension_daily` and `workspace_click_daily` therefore outlived the tenancy they describe, while DeleteOrganization's own doc said the audit trail was all that survived (F106).

Run *before* the organization is deleted, and that ordering is required rather than tidy: the workspaces are what name these rows, and the cascade takes the workspaces. After the delete there is nothing left to select them by.

Not a security fix. Every reader scopes to a live workspace_id, so these rows are unreachable rather than exposed; what they are is stale aggregate data with no owner, and a sentence that was not true.

func (*Queries) DeleteOutstandingRegistration added in v0.2.0

func (q *Queries) DeleteOutstandingRegistration(ctx context.Context, email string) (int64, error)

Clears whatever is outstanding for an address so a fresh attempt can take the slot.

Superseding rather than refusing, because the ordinary reason somebody registers twice is that the first mail never arrived. The old token stops working at the same moment, which is what makes this safe: there is never more than one live link per address.

func (*Queries) DeleteQRCodeByID added in v0.3.0

func (q *Queries) DeleteQRCodeByID(ctx context.Context, arg DeleteQRCodeByIDParams) (int64, error)

Removes one code. Scoped by workspace rather than by link, because the id is already unique and the service has resolved the link before it gets here; the workspace column is the tenancy check.

**`AND slug <> ”` is gone** (D183). It was what refused to delete the default code, back when the default *was* the empty slug; the refusal that replaces it is the service's, and it is about arithmetic rather than identity — a link's last code cannot be removed, whichever one it is.

The logo goes with the row, which is the whole of what D134 bought: no second statement, and no way for the two to come apart.

func (*Queries) DeleteRevokedAPIKeys

func (q *Queries) DeleteRevokedAPIKeys(ctx context.Context) (int64, error)

Reaper. Kept long enough to be visible in the key list after revocation, and long enough for the audit question above to be answerable.

func (*Queries) DeleteRoutingRule added in v0.2.0

func (q *Queries) DeleteRoutingRule(ctx context.Context, arg DeleteRoutingRuleParams) (*uuid.UUID, error)

Returns the destination so the caller can remove it in the same transaction. A rule target left behind would be an orphan row nothing reads and nothing can reach, accumulating one per deleted rule for the life of the link.

func (*Queries) DeleteRuleDestination added in v0.2.0

func (q *Queries) DeleteRuleDestination(ctx context.Context, arg DeleteRuleDestinationParams) error

A hard delete, not the soft delete `deleted_at` exists for. Nothing reports on a rule target and nothing can restore a deleted rule, so a tombstone here would be a row that only ever grows the table. The NOT EXISTS is a guard rather than a branch anything takes: only rule targets reach this query. It is here because the one row this must never delete is the link's own destination, and the cost of being wrong about that is a link that redirects nowhere.

func (*Queries) DeleteStaleEnvBlockedDestinations added in v0.2.0

func (q *Queries) DeleteStaleEnvBlockedDestinations(ctx context.Context, keep []string) (int64, error)

Retires environment entries the operator has since removed.

Scoped to source = 'env' and nothing else. A restart must never delete what an owner decided in the review queue, nor the shortener hosts seeded by migration, which is the one way a boot-time reconciliation could quietly undo a decision somebody made.

func (*Queries) DeleteTag

func (q *Queries) DeleteTag(ctx context.Context, arg DeleteTagParams) (int64, error)

func (*Queries) DeleteVariantRule added in v0.2.0

func (q *Queries) DeleteVariantRule(ctx context.Context, arg DeleteVariantRuleParams) (*uuid.UUID, error)

func (*Queries) DeleteWebhook added in v0.2.0

func (q *Queries) DeleteWebhook(ctx context.Context, arg DeleteWebhookParams) (int64, error)

The deliveries go with it, by the ON DELETE CASCADE 00600 declared. A webhook that has been removed and a delivery log that still names it would be a record of where events *used to* go, which is a different and less useful thing than the record of where they go.

func (*Queries) DeleteWorkspace added in v0.2.0

func (q *Queries) DeleteWorkspace(ctx context.Context, arg DeleteWorkspaceParams) (int64, error)

A real delete, not a soft one, and that is the decision D32 guards.

`links`, `tags` and `folders` cascade from here (00300_links.sql). Soft deleting instead would leave those rows behind and their aliases still serving redirects out of a workspace the dashboard says is gone, which is a worse outcome than the cascade — so the guard goes in front of the delete and the delete is honest about what it does.

func (*Queries) DetachAllTags

func (q *Queries) DetachAllTags(ctx context.Context, linkID uuid.UUID) error

func (*Queries) DisableUserMFA added in v0.3.0

func (q *Queries) DisableUserMFA(ctx context.Context, userID uuid.UUID) (int64, error)

Taking the second factor away.

Everything at once, because m53.md asks for exactly that: *clearing `mfa_enabled_at` clears the secret and every unused recovery code in the same transaction*. The codes are the caller's second statement — this one is the account row — and both are in one transaction, which is what makes "the account has no second factor" a state with no intermediate.

`mfa_last_step` goes too. It is meaningless without a secret, and leaving it would mean a later enrolment inherited a replay floor from a secret that no longer exists — an account that re-enrolled would find its first codes refused until the clock caught up.

func (*Queries) EnableUserMFA added in v0.3.0

func (q *Queries) EnableUserMFA(ctx context.Context, arg EnableUserMFAParams) (int64, error)

Enrolment, committed.

**`mfa_enabled_at` and `mfa_secret` are written together and only together**, which is m53.md's *half-enrolled is not a state this product has* stated as a single UPDATE. The secret is not parked on the row while the person fetches their phone: it is held by the enrolment session and reaches the database only in the statement that also says the second factor is on, and only after a code computed from it has verified.

`mfa_enabled_at IS NULL` in the predicate rather than checked beforehand, so enrolling an account that is already enrolled affects no rows instead of silently replacing a working secret with a different one. Two tabs finishing the same enrolment is the ordinary way that happens.

func (*Queries) EnqueueMail added in v0.2.0

func (q *Queries) EnqueueMail(ctx context.Context, arg EnqueueMailParams) error

The mail outbox. Queued on the request path, drained by the scheduler.

The message is stored rendered. Nothing here re-renders from a template, so a later template change cannot rewrite a mail somebody is already waiting for.

func (*Queries) EnqueueWebhookDeliveries added in v0.2.0

func (q *Queries) EnqueueWebhookDeliveries(ctx context.Context, arg EnqueueWebhookDeliveriesParams) (int64, error)

--- the queue ---------------------------------------------------------------

One statement fans one event out to every webhook that asked for it.

This runs inside a link write, so its cost on a workspace with no webhooks has to be one indexed lookup that returns nothing — which is what the partial index `webhooks_workspace_idx ... WHERE enabled` (00600) makes it.

The payload is rendered by the caller and stored rendered, for the reason the mail outbox stores its body rendered (D23): a change to what an event looks like must not rewrite an event that was already queued, and a row has to stay readable after the code that produced it is gone.

gen_random_uuid() rather than a v7 generated in Go, because the number of rows is not known until the SELECT runs. Nothing orders deliveries by id — the queue orders by next_attempt_at — so the time-sortability v7 buys is not spent here.

func (*Queries) EnsureWorkspaceSigningSecret added in v0.2.0

func (q *Queries) EnsureWorkspaceSigningSecret(ctx context.Context, arg EnsureWorkspaceSigningSecretParams) ([]byte, error)

Mint the secret if it is not there, and return whichever one is authoritative.

COALESCE rather than a read-then-write, so two people asking for a signed URL at the same moment cannot end up with signatures made under different keys: the second UPDATE sees the first one's committed value and keeps it. The caller generates the candidate bytes, because a random source belongs in the application rather than in an extension this schema does not require.

func (*Queries) EraseDeletedAccounts added in v0.3.0

func (q *Queries) EraseDeletedAccounts(ctx context.Context, arg EraseDeletedAccountsParams) ([]uuid.UUID, error)

The erasure pass. One batch, one statement, one transaction.

**What it scrubs is what deletion could not reach**, and there are two ways a row gets there. Two tables carry an address snapshot and **no** foreign key to `users` at all, because a record of the past that vanishes with its subject is not a record: `audit_logs` and `destination_disputes`. Two more do have one and are still out of reach — `notifications` (`00600:127`) and `invitations` (`01200:62`) — because the row belongs to a *different* person, so ending this account was never going to remove it and no retention window expires it.

*No foreign key* was the criterion when this pass scrubbed two tables. It is not the criterion now and saying so was wrong for a while: what finds all four is **a record about this person that ending the account does not remove**.

  • `audit_logs.actor_label` (`00600:137,150`). An audit trail that vanishes with its actor is not an audit trail.

  • `destination_disputes.created_by_label` **and** `decided_by_label` (`01600:64,68`). Two snapshots, not one: an account is as identifiable as the moderator of a dispute as it is as the filer of one, and F44 names this as the second table with no deletion path of any kind.

  • `audit_logs.metadata`'s `"email"` key (F177). **Seven** writers, counted against the tree on 2026-08-09 rather than recalled: `invite.go:437` (`invitation.created`) and `:860` (`invitation.redeemed`), `team/member.go:197`, `:258` and `:386` (`member.role_changed`, `member.removed`, `member.added`), and `instance/instance.go:642` (`instance.principal_moved`) and `:677` (the review grants). The address there is usually the *subject* of somebody else's action, so scrubbing it edits a record whose actor is still here — weighed and taken, because an erasure that reaches the label and stops at the detail one column over has not erased the person.

    Matched on the value, because there is no foreign key to match on: the column is jsonb and the address in it is a snapshot. Case-folded on both sides, since `invitation.created` stores what the administrator typed and that need not be the case the account was registered in. The accepted cost is a sequential scan of a partitioned table — the largest thing in this schema after analytics — paid once per batch, and only when there is a batch: see `HAVING count(*) > 0` on `batch` below, which is what makes an idle pass free rather than hourly.

    Written by the **same** UPDATE as `actor_label` rather than by a second one, and that is not a tidiness choice — see the note on `batch`.

  • `audit_logs.metadata`'s `"from"` **array** (F189). One writer: `instance/instance.go:642` puts the outgoing principals' addresses in an array beside the `"email"` key it also writes, so a prior instance principal who later deletes their account kept an address one key over from the one that was scrubbed. The scalar predicate above is the only shape F177 specified and an array is a different one, which is why this was a second finding rather than an oversight in the first.

    Rewritten element by element rather than dropped: the array says how many principals the role moved away from, and losing a member of it loses that count. Order is held by `WITH ORDINALITY`, because the addresses are the record of a single act and the sequence is part of what it says. A `from` that is not an array is left alone rather than erroring, so a record written by hand cannot fail the whole batch.

  • `notifications.data`'s `"email"` key **and the title beside it** (F188). `invite/invite.go:973` tells the inviter that their invitation was accepted, and both the detail and the sentence carry the address of the person who accepted. The row belongs to the *inviter*, so deleting the erased account's own notifications never reached it and nothing expires it — notifications are scoped to a reader, not swept by age.

    The title is rewritten by `replace` against `data->>'email'` rather than against the batch, and that is what keeps this out of the business of knowing how the sentence is worded: the two came from one value at one call site, so the address in the title is exactly the string the detail holds. Both CASEs read the pre-update row, so the title still finds the address after the same statement has replaced it in `data`.

    An **outstanding** invitation is left alone for the reason stated below, and this is deliberately the other answer: a notification is a record *about* a person delivered to somebody else, which is the same thing `audit_logs.metadata` is, and it gets the same treatment.

  • `invitations.email`, on the invitations the erased account **redeemed** (F181). `ListInvitations` carries no state predicate, so `/invites` renders every invitation an organization ever issued — redeemed ones included — and the address each was sent to. Nothing deletes those rows and no setting expires them, so an account deleted, erased and tombstoned everywhere else was still named in full on an ordinary dashboard page.

    `redeemed_by` is the join, not the address. What is scrubbed is the row this account *joined by*, which is a record about them; an **outstanding** invitation addressed to the same text is deliberately left alone, because it is an offer to an address rather than a record of a person, the address became reusable the moment the account was deleted, and blanking it would break the redemption comparison for whoever takes it next.

    Empty string rather than the tombstone, for the reason `users.email` is one: redemption compares a redeeming account's address against this column, and a placeholder that reads like a label is a value that comparison would then have to rule out. `invitations_outstanding_email_key` cannot collide on the blanks — it excludes redeemed rows, which is every row this reaches.

**The label is a constant and the ids survive** — D148, owner-set 2026-08-08. Nothing is derived from anything, so there is no derivation to reverse. Correlating one erased actor's entries is `audit_logs_actor_idx`, which is keyed on `actor_user_id` and never reads the label. The accepted cost is that a surviving uuid is pseudonymous rather than anonymous data, which `docs/SECURITY.md` states in those words.

**Re-entrant, because the two-leader window during a rolling deploy is a stated property of this scheduler** (`cmd/linkctrl/jobs.go:117-127`). `FOR UPDATE SKIP LOCKED` means a second leader takes a disjoint batch instead of waiting for the first, and the guard on each id-matched scrub makes a second pass over the same row a no-op rather than a rewrite. Three of those four compare against the tombstone they write; `invitations` blanks its column instead of labelling it, so its guard is an emptiness test on `i.email` beside `i.redeemed_at IS NOT NULL` — the same claim in the vocabulary that column uses. Running the pass twice and diffing is what the test asserts. The **three** value-matched scrubs are re-entrant for a different reason and it is worth stating: after the first pass the address they matched on no longer exists in the column, so the second pass finds nothing to rewrite.

Ordered by `deleted_at`, oldest first, which is the order `users_pending_erasure_idx` stores and the order the requests arrived in.

`pending` carries the address as well as the id, because two of the scrubs below have nothing else to match on and the final UPDATE blanks it. That is safe rather than lucky: every CTE in a statement reads one snapshot, so the value here is the pre-erasure one no matter which order the executor runs them in. The account row itself, scrubbed in place. It survives, which is the whole difference between `anonymized_at` and `deleted_at`: foreign keys and audit records go on pointing at a row that identifies nobody.

`email` becomes the empty string rather than a placeholder address. No live query can reach it — every read of `users` filters `deleted_at IS NULL` — and the partial unique index excludes the row, so the address it held was already reusable the moment the account was deleted.

func (*Queries) GetAPIKeyByPrefix

func (q *Queries) GetAPIKeyByPrefix(ctx context.Context, prefix string) (GetAPIKeyByPrefixRow, error)

The verification lookup, on the unique prefix index, joined with the user so authentication is one round trip. Revoked and expired keys are returned rather than filtered out: the caller distinguishes them so the response can say which it was, and a deleted user's key resolves to no row at all.

grace_expires_at comes back for the same reason revoked_at does: a rotated predecessor stops verifying when its window closes, and that refusal is decided here rather than by the housekeeping job that later writes revoked_at.

owner_is_member is the membership the key leans on, asked here so that authentication stays one round trip. A key acts *as its owner*, and an owner with no membership covering the key's scope has no authority for it to act with — so the credential is invalid rather than merely powerless. The predicate matches GetUserPermissions exactly: an organization-wide membership covers every workspace, a workspace-scoped one covers its own, and an unpinned key is covered only by an organization-wide membership, because NULL = NULL is not true.

**Which organization it asks about is no longer the key's column** (M54). A pinned key still asks about the organization it names, and that branch is unchanged. An account-wide one has no column to ask about: it asks whether the owner holds an organization-wide membership in *any* organization this key has not been barred from, which is the same question one step earlier — can this credential reach anywhere at all. Which organization it then lands in is ResolveOrganizationForAPIKey's, and the membership covering that one is guaranteed by the same join, so the precise test and this coarse one cannot disagree.

Returned as a column rather than joined into the WHERE clause for the reason revoked and expired keys are returned rather than filtered: the caller decides what each state means, and a refusal a reader can see beside the others is worth more than a row that silently fails to exist.

func (*Queries) GetAPIKeyForRotation added in v0.2.0

func (q *Queries) GetAPIKeyForRotation(ctx context.Context, id uuid.UUID) (GetAPIKeyForRotationRow, error)

The predecessor, locked, so two rotations of one key serialize instead of racing.

FOR UPDATE rather than an optimistic conditional write: the loser of a race should be told "this key has already been rotated" by the check that follows, which is a sentence somebody can act on, not have its transaction rolled back by a unique-index violation on a column it never named.

owner_is_member and owner_status come back for the reason revoked_at does: they are re-read here, under the lock, so a membership removed or an account deactivated between authentication and this statement wins. Without them a removal racing a rotation leaves behind a successor the removal was meant to kill, on a chain that can rotate again. A soft-deleted account reads 'deleted' rather than NULL, so the one comparison at the call site covers it and no branch depends on a scan of an absent row.

func (*Queries) GetAPIKeyReach added in v0.3.0

func (q *Queries) GetAPIKeyReach(ctx context.Context, id uuid.UUID) (GetAPIKeyReachRow, error)

The reach of a key named by id, for the administrator's arm of Revoke (M54).

Read before the write because the write depends on the answer: a pinned key is revoked outright and an account-wide one has an organization cut out of it, and those are different statements against different tables. Returns the owner too, because whichever it turns out to be, the record has to name whose credential was stopped.

No organization predicate, deliberately. Scoping the *read* would make an account-wide key unfindable by every administrator, since it matches none of them; the authorization is at the call site, which refuses unless the owner is a member of the organization the actor holds authority in. A key whose owner has never been in the actor's organization is ErrNotFound, exactly as an unknown id is.

func (*Queries) GetAutomationRule added in v0.2.0

func (q *Queries) GetAutomationRule(ctx context.Context, arg GetAutomationRuleParams) (AutomationRule, error)

func (*Queries) GetBlockedDestination added in v0.2.0

func (q *Queries) GetBlockedDestination(ctx context.Context, host string) (GetBlockedDestinationRow, error)

Reads one entry by its exact host.

The decision path's read, and deliberately not MatchBlockedDestination: that one walks a host and every parent of it and answers with the longest match, which is the right question when *judging* a destination and the wrong one when acting on a dispute. The dispute already names the row it is about — destination_disputes.blocked_host, written when it was filed — so the only thing left to ask is whether that row is still there and who owns it (03300).

No rows means the entry has gone since the dispute was filed, which the caller reports rather than papering over: an allow that deleted nothing must not be recorded as one that did.

func (*Queries) GetBuiltinRoleBySlug added in v0.2.0

func (q *Queries) GetBuiltinRoleBySlug(ctx context.Context, slug string) (GetBuiltinRoleBySlugRow, error)

func (*Queries) GetCampaign added in v0.2.0

func (q *Queries) GetCampaign(ctx context.Context, arg GetCampaignParams) (Campaign, error)

Workspace-scoped like GetLink and GetFolder, and for the same reason: the wrong workspace returns no rows rather than a row the caller must remember to reject.

func (*Queries) GetClickBudget added in v0.2.0

func (q *Queries) GetClickBudget(ctx context.Context, linkID uuid.UUID) (GetClickBudgetRow, error)

What the dashboard shows beside a gated link. Never on the redirect path.

func (*Queries) GetDefaultDomainSettings

func (q *Queries) GetDefaultDomainSettings(ctx context.Context, domainID uuid.UUID) (GetDefaultDomainSettingsRow, error)

One domain's settings and where its root points.

**The filter its own comment promised** (M40). It read `WHERE is_default`, which was the whole truth while an instance had exactly one domain and became a way of asking the wrong row the moment it had several: a verified custom hostname has a root of its own, and reading its settings through a predicate that can only ever return the default would answer about somebody else's hostname.

Not scoped by owner, like every other statement addressed by id in this schema. link.Service has already judged the actor against the row.

func (*Queries) GetDefaultQRCode added in v0.3.0

func (q *Queries) GetDefaultQRCode(ctx context.Context, arg GetDefaultQRCodeParams) (GetDefaultQRCodeRow, error)

The code an untagged scan resolves through (M50's reopening, D183).

One flagged row at most, which `qr_codes_link_default_key` (04400) is what makes true: a partial unique index over `link_id WHERE is_default`. No rows means the link's default code has never been written down — the synthesised default D139 describes — and the service answers for it at the product style rather than reporting an absence.

**The empty slug is a fallback rather than the answer, and it is the second half of what makes this migration safe.** 03700's identity was `slug = ”`, and a row can still arrive carrying it and not the flag: written by the previous release during a rolling deploy, when `is_default` is a column it does not know about, or written by hand. Reading the flag alone would report such a link as having no default at all, and the next style write would then insert a second unnamed row against `qr_codes_link_slug_key`. Preferring the flag and falling back to the empty slug costs one ORDER BY and makes both spellings of the same fact resolve to the same row. `LIMIT 1` because the two can name different rows only on a link that has more than one code, where the empty slug does not occur at all.

func (*Queries) GetDestinationDispute added in v0.2.0

func (q *Queries) GetDestinationDispute(ctx context.Context, id uuid.UUID) (GetDestinationDisputeRow, error)

func (*Queries) GetDomainBotSettings added in v0.2.0

func (q *Queries) GetDomainBotSettings(ctx context.Context, id uuid.UUID) (GetDomainBotSettingsRow, error)

One domain's bot policy, by id.

Read on the management path only, and only when a link's own setting is being changed: the service has to know whether the domain enforces before it can tell the caller their `off` will not be honoured. The redirect path never runs this — it gets the same two columns from ResolveAliasForRedirect's join, which is the whole reason that join exists.

func (*Queries) GetDomainByHostname added in v0.2.0

func (q *Queries) GetDomainByHostname(ctx context.Context, lower string) (Domain, error)

Matches domains_hostname_key exactly — lower(hostname) among the undeleted — so the availability check and the unique index cannot disagree about which names collide.

func (*Queries) GetDomainByID added in v0.2.0

func (q *Queries) GetDomainByID(ctx context.Context, id uuid.UUID) (Domain, error)

The row the ownership check is made against, so it carries both owner columns.

func (*Queries) GetFolder added in v0.2.0

func (q *Queries) GetFolder(ctx context.Context, arg GetFolderParams) (Folder, error)

Workspace-scoped like GetLink, and for the same reason: the wrong workspace returns no rows rather than a row the caller must remember to reject. This is also the only check that a link is being filed into a folder of its own workspace — `links.folder_id` has a foreign key to `folders(id)` and nothing in it mentions tenancy.

func (*Queries) GetInvitationByTokenHash added in v0.2.0

func (q *Queries) GetInvitationByTokenHash(ctx context.Context, tokenHash []byte) (GetInvitationByTokenHashRow, error)

Redemption's only lookup, and the row it locks.

FOR UPDATE OF i serializes two redemptions of the same token: the second blocks until the first commits and then reads redeemed_at set, so single-use is enforced by the database rather than by a check-then-act in Go. The joined tables are not locked — they are read for their labels, and locking a role row would block every other invite that names it.

Expiry and revocation are deliberately NOT in the WHERE clause. The caller has to tell "no such token" from "expired" apart to decide what to log, and it answers all of them identically to the person redeeming (decision D27).

func (*Queries) GetJobStaleness added in v0.2.0

func (q *Queries) GetJobStaleness(ctx context.Context) ([]GetJobStalenessRow, error)

How long ago each job last succeeded, in seconds.

Read from the database rather than kept in the process, and that is the whole point of it. `linkctrl_job_last_success_timestamp_seconds` is set by whichever replica ran the job and resets to absent on restart, so on a multi-replica deployment it answers differently depending on which one Prometheus scraped and it forgets everything a rolling deploy touched. job_state is shared, so every replica reports the same number and a restart does not make a stalled job look healthy.

A job that has never succeeded is excluded rather than reported as infinitely stale. Inventing a series for it would make every fresh instance look broken for its first few seconds, and an absent series is what the alert recipe in docs/operations.md is written against.

func (*Queries) GetJobWatermark

func (q *Queries) GetJobWatermark(ctx context.Context, job string) (*time.Time, error)

--- job bookkeeping --------------------------------------------------------- The point a job is known to have completed through. Rollups recompute rather than accumulate, so this is not a correctness dependency for a run that happens on schedule — it exists for the run that does not. Without it, RunRecent covered a fixed yesterday-and-today window, and any downtime that spanned a UTC day left that day with no rollup and nothing to notice it: the raw events were still there, but nothing ever aggregated them again.

func (q *Queries) GetLink(ctx context.Context, arg GetLinkParams) (Link, error)

Workspace-scoped by design. Passing the workspace here rather than checking it after the fetch makes cross-tenant reads impossible to write by accident: the wrong workspace returns no rows rather than a row the caller must remember to reject.

func (*Queries) GetLinkByAlias

func (q *Queries) GetLinkByAlias(ctx context.Context, arg GetLinkByAliasParams) (Link, error)

func (*Queries) GetLinkDimensions

func (q *Queries) GetLinkDimensions(ctx context.Context, arg GetLinkDimensionsParams) ([]GetLinkDimensionsRow, error)

func (*Queries) GetLinkPasswordHash added in v0.2.0

func (q *Queries) GetLinkPasswordHash(ctx context.Context, id uuid.UUID) (*string, error)

The gates a link can put in front of its destination (M35).

Three of these run on the redirect path and one does not, and the split is what keeps the gates off the budget of every link that does not use them. Nothing here is consulted for a link whose snapshot says it is ungated; the snapshot carries the flags, the flags decide, and only then does anything below execute. The argon2id hash, read only on the password-submit path.

**The cached snapshot never carries this.** It carries a bare boolean, so a Redis dump — or a snapshot payload logged by accident — cannot yield an offline cracking target for every password link on the instance. The price is this query, and it is paid once per submitted password rather than once per visit: a GET that renders the challenge never runs it, and a link with no password never reaches it.

Addressed by link id rather than by alias, because the id came out of the snapshot the resolver already produced and re-deriving it from the alias would be a second lookup of a row we have already identified.

func (*Queries) GetLinkQRDimensions added in v0.3.0

func (q *Queries) GetLinkQRDimensions(ctx context.Context, arg GetLinkQRDimensionsParams) ([]GetLinkQRDimensionsRow, error)

The per-QR-code breakdown (M50).

**A filter over GetLinkDimensions' rows, not a rollup of its own.** Every value here was written by RollupDimensionDaily's ordinary `referrer` pass, because a scan's code is stored *as* its referrer value — `qr:<slug>` for a scan that named a code, and the bare `qr` for one that named none, which the reader counts against whichever code is the default (D183). So this milestone added no pass over click_events, no column and no dimension name: the thing that made a per-campaign rollup too expensive to include in this phase is the thing this does not do.

It is a separate statement rather than a reuse of GetLinkDimensions because that one is bounded at twenty rows ordered by clicks, and a link whose busiest referrers are twenty real hostnames would lose its own codes off the end of its own breakdown. Same table, same index, same shape — only the predicate and the bound differ, and the bound is domain.MaxQRCodesPerLink + 1 because the default code is one more than the cap counts.

`value = 'qr' OR value LIKE 'qr:%'` cannot collide with a real referrer. The column otherwise holds hostnames and the `direct` sentinel, and a colon is not a character a hostname may contain.

func (*Queries) GetLinkStats

func (q *Queries) GetLinkStats(ctx context.Context, arg GetLinkStatsParams) ([]GetLinkStatsRow, error)

Reads the rollup, never the raw events. This is what keeps analytics under the 2s target as click_events grows into the tens of millions.

func (*Queries) GetLinkTags

func (q *Queries) GetLinkTags(ctx context.Context, linkID uuid.UUID) ([]GetLinkTagsRow, error)

func (*Queries) GetMembership added in v0.2.0

func (q *Queries) GetMembership(ctx context.Context, arg GetMembershipParams) (GetMembershipRow, error)

One membership, scoped by organization so an id from elsewhere is indistinguishable from one that never existed.

FOR UPDATE OF m: every caller is about to re-role or delete this row, and the rank check that decides whether they may is read from it. Without the lock, two administrators acting at once could each read a state the other is changing — the check-then-act that the last-owner refusal exists to prevent.

func (*Queries) GetNotification added in v0.3.0

func (q *Queries) GetNotification(ctx context.Context, arg GetNotificationParams) (Notification, error)

One row of the actor's own inbox, scoped by user_id like every statement around it: somebody else's notification is "no rows" rather than a 403 that confirms the id exists.

Read by the click-through (M48). Where a notification leads is computed from its `kind` and its `data`, and both have to come off the row — a destination carried on the request would be a redirect target the caller chose.

func (*Queries) GetOrganization added in v0.2.0

func (q *Queries) GetOrganization(ctx context.Context, id uuid.UUID) (Organization, error)

The organization being deleted, read after LockOrganizations has locked it.

Its name and slug are read here because the audit record has to carry them: once the row is gone that record is the only remaining trace of what was deleted, exactly as it is for a workspace.

func (*Queries) GetOrganizationMember added in v0.2.0

func (q *Queries) GetOrganizationMember(ctx context.Context, arg GetOrganizationMemberParams) (GetOrganizationMemberRow, error)

Whether a user is in an organization at all, for the grant path: workspace-scoped access is given to somebody who is already a member, and this is what establishes that.

**Any** membership counts, organization-wide or workspace-scoped. Requiring an organization-wide one would be a dead end: somebody left holding only a workspace-scoped membership could never be given a second workspace, because re-inviting them is refused as already-a-member. Under D31 every grant adds, so widening a scoped member to a second workspace is the same kind of act as the first grant was.

The organization-wide row wins the tiebreak so the label and role this returns are the person's broadest, which is what a control naming them should show.

func (*Queries) GetOrganizationName added in v0.2.0

func (q *Queries) GetOrganizationName(ctx context.Context, id uuid.UUID) (string, error)

What to call the organization in an invitation. A primary-key lookup on a path that is already writing a row, rather than carrying a name on every identity for the one surface that needs it.

func (*Queries) GetPasswordResetByTokenHash added in v0.3.0

func (q *Queries) GetPasswordResetByTokenHash(ctx context.Context, tokenHash []byte) (GetPasswordResetByTokenHashRow, error)

The reset lookup, inside the transaction that spends the row.

FOR UPDATE, so two submissions of the same link serialize and the second sees the row the first consumed. The join is what makes the account's own state reachable in one round trip: `status` and `password_hash` are both refusals this path has to make, and reading them separately would leave a gap between the check and the write.

func (*Queries) GetPendingRegistrationByTokenHash added in v0.2.0

func (q *Queries) GetPendingRegistrationByTokenHash(ctx context.Context, tokenHash []byte) (PendingRegistration, error)

Verification's lookup, inside the transaction that spends the row.

FOR UPDATE, so two clicks on the same link serialize and the second sees the row the first consumed.

func (*Queries) GetQRCode added in v0.2.0

func (q *Queries) GetQRCode(ctx context.Context, arg GetQRCodeParams) (GetQRCodeRow, error)

**`q.*` is gone from the four reads below, and that is M50.5 rather than style.** *(Three when M50.5 wrote this; `GetDefaultQRCode` is D183's, and it carries the same explicit list for the same reason.)* `qr_codes` now carries a `logo bytea` (03800, D134) bounded at qr.MaxLogoStoredBytes — a little over a megabyte a row — and a link may hold domain.MaxQRCodesPerLink of them. A star projection would fetch every one of those bytes to draw a list of names, so the reads carry an explicit column list and report the logo as the one fact a reader of the list needs: **whether there is one**. `logo IS NOT NULL` is answered from the row's TOAST pointer without detoasting the value, so asking costs nothing.

The bytes themselves are read by nothing here. Nothing in M50.5 serves a stored logo back — the two operations are set and clear — and M50.6, which composites one into a picture, is where a query that reads them belongs. One code of a link's, by slug. No rows means the code does not exist, and the service reports that.

**The default code is not reachable here and that is the point** (D183). It used to be `slug = ”`; it is now whichever row carries `is_default`, which is GetDefaultQRCode's job, because a caller that wanted "the default" and passed the empty string would silently match nothing at all now that no row holds it.

func (q *Queries) GetQRCodeLogo(ctx context.Context, arg GetQRCodeLogoParams) ([]byte, error)

The bytes, for the one thing they are for (M50.6).

**The only read in this file that projects the column**, and it is separate from GetQRCode rather than folded into it for exactly the reason the three reads above stopped saying `q.*`: drawing a list of twenty names must not fetch twenty images. This is called once, by a surface that is about to composite one code's logo into one picture, and only for a code whose `has_logo` already said there is something to fetch.

NULL comes back for a code with no logo, which the service reads as "nothing to draw" rather than as an error: `has_logo` and this can disagree by exactly one concurrent clear, and the honest answer to that race is the picture without the logo.

func (*Queries) GetRecentClicks

func (q *Queries) GetRecentClicks(ctx context.Context, arg GetRecentClicksParams) ([]GetRecentClicksRow, error)

The live-activity feed. Bounded and index-backed on (link_id, occurred_at).

func (*Queries) GetRoleBySlug

func (q *Queries) GetRoleBySlug(ctx context.Context, slug string) (Role, error)

func (*Queries) GetRoutingRule added in v0.2.0

func (q *Queries) GetRoutingRule(ctx context.Context, arg GetRoutingRuleParams) (GetRoutingRuleRow, error)

func (*Queries) GetSalt

func (q *Queries) GetSalt(ctx context.Context, validOn time.Time) ([]byte, error)

Analytics: salts, rollups and reads.

func (*Queries) GetSessionByTokenHash

func (q *Queries) GetSessionByTokenHash(ctx context.Context, tokenHash []byte) (GetSessionByTokenHashRow, error)

Joined with the user so validating a session is one round trip on a path that runs for every authenticated request. Filters revoked and deleted here rather than in Go, so a revoked session cannot be resurrected by a caller that forgets to check.

func (*Queries) GetTagByName

func (q *Queries) GetTagByName(ctx context.Context, arg GetTagByNameParams) (Tag, error)

func (*Queries) GetUserByEmail

func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error)

Comparison is on the generated email_lower column, so callers cannot accidentally do a case-sensitive lookup and create a duplicate account.

func (*Queries) GetUserByID

func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error)

func (*Queries) GetUserMFA added in v0.3.0

func (q *Queries) GetUserMFA(ctx context.Context, id uuid.UUID) (GetUserMFARow, error)

A second factor (M53): TOTP, enrolment, recovery codes, and the step between a right password and a session.

Three groups of statements. The enrolment pair writes `users.mfa_secret` and `users.mfa_enabled_at` — the columns `00200_identity.sql` has carried since the first migration and only M52's erasure sweep has ever touched. The recovery-code statements are a hashed single-use credential, the fifth thing in this schema shaped that way. And the pending-login statements are the state machine that makes "no session token exists until the second factor is verified" a property of the database rather than of a handler's control flow. Everything the second factor needs about one account, and nothing else.

Not `SELECT *`: the enrolment and challenge paths have no business holding a password hash, and a narrow row is what keeps that true as columns are added.

func (*Queries) GetUserPermissions

func (q *Queries) GetUserPermissions(ctx context.Context, arg GetUserPermissionsParams) ([]string, error)

The RBAC evaluator's source of truth. Returns every permission a user holds in a workspace, via their organization membership and its role.

A NULL memberships.workspace_id means the membership covers every workspace in the organization, which is what Phase 1 always creates.

func (*Queries) GetUserRoleInWorkspace

func (q *Queries) GetUserRoleInWorkspace(ctx context.Context, arg GetUserRoleInWorkspaceParams) (GetUserRoleInWorkspaceRow, error)

func (*Queries) GetVariantRule added in v0.2.0

func (q *Queries) GetVariantRule(ctx context.Context, arg GetVariantRuleParams) (GetVariantRuleRow, error)

func (*Queries) GetWebhook added in v0.2.0

func (q *Queries) GetWebhook(ctx context.Context, arg GetWebhookParams) (GetWebhookRow, error)

func (*Queries) GetWorkspaceDefaultDomain

The hostname a new link goes on when the caller names none.

**This is the filter the name promised and the query never had** (M40). It read `WHERE is_default` with no workspace argument at all, so every workspace on the instance got the same answer and the word "workspace" in the name was describing an intention rather than a predicate.

A workspace's own *verified* hostname wins over the instance default, which is what registering one is for; the instance default is the fallback and is what every workspace without one still gets, unchanged. Organization-owned hostnames sit between the two — every workspace in the organization may use one, so it is more specific than the instance and less than a workspace's own.

**Verified only.** An unverified hostname is not a routing target, so putting a link on it would mint a short URL that resolves nowhere; the ordering below cannot reach one because the WHERE clause has already excluded it.

Ties are broken by verified_at then id, so the answer is stable: a workspace that verifies a second hostname does not silently move its new links onto it. organization_id and workspace_id are selected because they are the domain's *scope*, and the scope is what decides whether an alias collision on it could involve a workspace the caller cannot see. A refusal that cannot tell a shared namespace from a private one has to be worded for the worst case or say nothing useful at all (F23).

func (*Queries) GetWorkspaceInOrganization added in v0.2.0

func (q *Queries) GetWorkspaceInOrganization(ctx context.Context, arg GetWorkspaceInOrganizationParams) (Workspace, error)

One workspace, scoped by organization so an id belonging to another tenant is indistinguishable from one that does not exist.

FOR UPDATE: both callers — rename and delete — are about to write this row or the rows that cascade from it, and delete reads a link count that must not change underneath the decision.

func (*Queries) GetWorkspaceSigningSecret added in v0.2.0

func (q *Queries) GetWorkspaceSigningSecret(ctx context.Context, id uuid.UUID) ([]byte, error)

The HMAC key for one workspace, read on the redirect path only for links whose snapshot says they require a signature — and cached in process by the caller, so a signed link costs one query per workspace per process rather than one per request. NULL means the workspace has never minted one, which means nothing in it can carry a valid signature.

func (*Queries) GetWorkspaceStats

func (q *Queries) GetWorkspaceStats(ctx context.Context, arg GetWorkspaceStatsParams) ([]GetWorkspaceStatsRow, error)

func (*Queries) GetWorkspaceTotals

func (q *Queries) GetWorkspaceTotals(ctx context.Context, arg GetWorkspaceTotalsParams) (GetWorkspaceTotalsRow, error)

Summing daily uniques over-counts anyone visiting on more than one day. Reported as "unique visitors per day, summed" in the UI rather than presented as a distinct-person count, because the exact figure cannot be recovered once the salts are purged. That is the intended trade.

func (*Queries) GrantInstancePermission added in v0.2.0

func (q *Queries) GrantInstancePermission(ctx context.Context, arg GrantInstancePermissionParams) (int64, error)

Confer one instance-level permission on one account.

Idempotent. Re-conferring what somebody already holds is the ordinary result of two administrators doing the same obvious thing, and it must not turn into an error that reads like a refusal; the original granted_by and granted_at stand, because the first grant is the one that happened.

It returns the row count for a reason that has nothing to do with idempotence: the SELECT finds no row for a slug that does not exist, so a typo would confer nothing and report success. On the setup path that means an instance that has been claimed and has nobody who can administer it, which is the one outcome this whole table exists to prevent. The caller distinguishes 0 from 1 by reading the count against a permission it already knows the account did not hold; see internal/auth's grantInstancePrincipal.

func (*Queries) HostHasAllowedDispute added in v0.2.0

func (q *Queries) HostHasAllowedDispute(ctx context.Context, host string) (bool, error)

Whether the instance owner has allowed this host (M32).

Read at exactly one call site — internal/link's feed step — and that confinement is the whole safety argument. It suppresses the third-party reputation feed for a host the owner already decided about, which is what makes a feed verdict owner-overridable without 01500 growing the allow column it deliberately does not have.

It cannot widen anything else. The three tiers above the feed have all returned by the time this runs, and M31 refuses to file a dispute about any refusal but a low-confidence one, so no row here can carry an unappealable or embedded-tier reason code to be read as permission.

Equality rather than the blocklist's candidate walk: allowing 'evil.example' says nothing about 'login.evil.example', and 01700's partial index matches this predicate exactly.

func (*Queries) InsertAuditLog added in v0.2.0

func (q *Queries) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error

Audit log. Append-only: there is no update and no delete here, and that is the point of the table. Rows leave only when retention drops a whole partition.

func (*Queries) InsertDestinationDispute added in v0.2.0

func (q *Queries) InsertDestinationDispute(ctx context.Context, arg InsertDestinationDisputeParams) (DestinationDispute, error)

Blocked-attempt disputes (M31).

Read and written on the management path only. Like the blocklist it argues with, nothing here is reachable from the redirect tree: a dispute is about what may be *stored*, and a link that was refused at creation never became a row for a visitor to resolve. Files one dispute.

Two unique partial indexes decide whether this is a duplicate, and both do it in the database rather than in a check-then-insert two requests can both pass. 01600's is on (host) WHERE status = 'open' — one open dispute per host as typed. 03300's is on (blocked_host) WHERE status = 'open' AND blocked_host <> ” — one open dispute per *blocklist row*, so a caller cannot put the same decision in front of the owner once per subdomain of it.

@blocked_host is the row the refusal matched, which is routinely a parent of @host. Empty when the rule is computed from the URL rather than held on the list, and the second index skips those: every one of them would carry the same key, and one open homograph dispute must not lock out every other.

func (*Queries) InsertMFARecoveryCode added in v0.3.0

func (q *Queries) InsertMFARecoveryCode(ctx context.Context, arg InsertMFARecoveryCodeParams) error

One code of a set. Called ten times inside the transaction that issues them, rather than as one multi-row insert, because the hashes are computed one at a time and a loop over a prepared statement is what sqlc gives without a bespoke array parameter for a fixed ten rows.

func (*Queries) InsertNotification added in v0.2.0

func (q *Queries) InsertNotification(ctx context.Context, arg InsertNotificationParams) error

Notifications. The table shipped dormant in 00600; nothing here adds a column, per the rule that a dormant table's structure goes in its jsonb until the feature that needs a column arrives. `data` carries whatever a kind needs.

func (*Queries) IsAliasTaken

func (q *Queries) IsAliasTaken(ctx context.Context, arg IsAliasTakenParams) (bool, error)

Consulted by BOTH create paths — generated aliases before insert, and user-supplied aliases as validation — and by alias changes.

No deleted_at filter on the links branch, deliberately: a soft-deleted row holds its alias for the whole trash window, so a link deleted by accident can be restored under its own name. The partial unique index cannot enforce that (it ignores trashed rows), so this check is the enforcement and the index remains the guarantee against live-row races only.

func (*Queries) ListAPIKeyOrgRevocations added in v0.3.0

func (q *Queries) ListAPIKeyOrgRevocations(ctx context.Context, apiKeyIds []uuid.UUID) ([]ListAPIKeyOrgRevocationsRow, error)

Which organizations have been cut out of which of these keys (F178).

The other half of the reach revocation M54 built. An administrator can bar an account-wide key from their organization and the key's owner had no way to learn it: the list above reports one reach — pinned or account-wide — and a credential that had silently stopped resolving into one tenant read exactly as it did the day before. The audit record that says why is written in the administrator's organization, which the owner may hold no `audit.read` in.

Keyed on the ids already listed rather than on the owner, so it is one round trip after the list and not one per key, and so it cannot return a bar on a key the caller was not shown.

The organization's name is joined because an id answers nobody's support question, and it discloses nothing new: a bar only ever exists on a key whose owner held an organization-wide membership there when it was written.

Soft-deleted organizations are not filtered out. The bar is a fact about the key, it outlives the tenant, and hiding the row would make a key look unrestricted when it is not.

func (*Queries) ListAPIKeysForUser

func (q *Queries) ListAPIKeysForUser(ctx context.Context, userID uuid.UUID) ([]ListAPIKeysForUserRow, error)

Revoked keys are included. "Which keys existed and when were they revoked" is the question asked after an incident, so they are listed until the reaper removes them.

workspace_id is selected because NULL is a state the owner chose (M44): a key bound to one workspace and a key valid across the organization look identical without it. organization_id is selected for the same reason one tier up (M54): NULL there is account-wide and non-NULL is pinned, and a list that omitted it would show two credentials with different reach as one row shape.

**Owner alone, no organization predicate** (M54, closing F75). It used to carry `AND organization_id = $2` while RevokeAPIKey carried only the owner, and the two statements disagreeing about which keys an actor reaches was the whole of that finding: a key listed nowhere still revoked, so 204-versus-404 answered "is this id one of mine, elsewhere". They agree now because the question they both ask is the same one — whose key is this — and an account-wide key has no organization to filter on in the first place.

func (*Queries) ListAuditLogs added in v0.2.0

func (q *Queries) ListAuditLogs(ctx context.Context, arg ListAuditLogsParams) ([]AuditLog, error)

Newest first, keyed on (occurred_at, id) so the cursor is a position rather than an offset: an event written while a reader is paginating shifts every offset by one, and a keyset cursor is unaffected by it.

The row-comparison predicate is what makes that hold. Comparing the columns separately -- occurred_at < c OR (occurred_at = c AND id < i) -- is the same logic, and the planner does not always recognise it as a range scan on the (organization_id, occurred_at DESC) index.

Scoped by organization, never by the workspace the reader happens to be *acting in*: an audit log that narrowed itself to the current workspace would hide exactly the actions worth reviewing. That is M21's argument and it still holds; what it never said is that the reader's own authority does not bound the rows either.

It does now (F31). `org_wide` is true when the reader holds audit.read from an organization-wide membership, which is the only membership that reaches the organization-wide scope (auth.MembershipAuthority, D44) — such a reader sees every row, exactly as before. A reader whose audit.read comes only from workspace-scoped memberships sees the rows of those workspaces and nothing else, because a workspace-scoped membership grants authority over its own workspace and not over the organization.

Rows with a NULL workspace_id are organization-level acts, and `= ANY` is false against NULL, so a workspace-scoped reader does not see them. That is the same asymmetry MembershipAuthority.In(nil) enforces for writes, arriving here for reads.

func (*Queries) ListAutomationRules added in v0.2.0

func (q *Queries) ListAutomationRules(ctx context.Context, workspaceID uuid.UUID) ([]AutomationRule, error)

func (*Queries) ListBuiltinRoles added in v0.2.0

func (q *Queries) ListBuiltinRoles(ctx context.Context) ([]ListBuiltinRolesRow, error)

The four seeded roles, most powerful first. Feeds the invite form's role choices, filtered by the inviter's own rank in the service (decision D28).

func (*Queries) ListCampaigns added in v0.2.0

func (q *Queries) ListCampaigns(ctx context.Context, workspaceID uuid.UUID) ([]ListCampaignsRow, error)

Every campaign in the workspace, with how many live links carry it.

The count is a grouped scan of links_campaign_idx rather than a correlated subquery per campaign, exactly as ListFolders counts filed links: a workspace with many campaigns costs one pass instead of one index probe each.

Unpaginated, and bounded instead by domain.MaxCampaignsPerWorkspace. A campaign list is a picker as much as it is a page — the link form offers it — and a picker that paginates is a picker nobody can choose from.

func (*Queries) ListDestinationDisputes added in v0.2.0

func (q *Queries) ListDestinationDisputes(ctx context.Context, arg ListDestinationDisputesParams) ([]ListDestinationDisputesRow, error)

The queue, newest first, keyset on (created_at, id).

Instance-wide rather than scoped to the reader's organization, because the list a decision acts on is instance-wide (01500) and a queue narrower than the authority it exercises would hide rows the reader is nonetheless deciding for. The permission is what bounds who sees it.

@open_only lets the page show the work and the archive from one query, which is the same shape ListNotifications' unread filter has.

The LEFT JOIN carries the blocklist entry's **source**, which is what decides whether an allow can do anything (F42). `liftableRules` says the *rule* is list-backed; it does not say the entry behind this particular refusal is one a decision may delete. An `env`-sourced entry comes from LINKCTRL_DESTINATION_BLOCKLIST and is rewritten at every boot, so removing it would be undone by the next restart and `entryToLift` refuses — while the page drew the Allow button from the rule alone and the operator found out by clicking. LEFT, because a refusal computed from the URL has no entry at all and must stay in the queue.

func (*Queries) ListDomains added in v0.2.0

func (q *Queries) ListDomains(ctx context.Context, arg ListDomainsParams) ([]ListDomainsRow, error)

Every domain the caller may use: the instance default, whatever their organization owns, and whatever their own workspace owns.

Another workspace's hostname is absent rather than present and unmanageable. A list that showed it would disclose which hostnames a neighbouring workspace has registered, and the registration is the only thing there is to disclose at this milestone.

Ordered default-first, then by hostname: the default is the one every link is on today, so it belongs at the top rather than wherever its placeholder name happens to sort.

func (*Queries) ListDueAutomationRules added in v0.2.0

func (q *Queries) ListDueAutomationRules(ctx context.Context, rowLimit int32) ([]ListDueAutomationRulesRow, error)

--- evaluation --------------------------------------------------------------

The rules one run considers, least recently looked at first.

Bounded by the caller at domain.AutomationRulesPerRun, and ordered so the cap starves nobody — which is a claim this statement can now make, because `last_checked_at` moves for **every** rule a run reached and not only for the ones that fired. The rules a capped run skipped keep the older cursor and go first next time. A cap without this order would evaluate whichever workspace happened to sort first, forever.

**Ordering on `last_fired_at` is what F83 was**, and the difference is not cosmetic: that column moves only on a firing, idle is exactly what keeps it old, and so the hundred oldest were a fixed set and rule 101 was never evaluated on any run. The two columns are separate facts and 03100 separates them.

The organization is joined in here rather than fetched per rule, because every rule that fires with a `notify` action needs it and N+1 lookups to assemble a batch is the wrong shape. Walks automation_rules_due_idx, as rebuilt by 03100.

func (*Queries) ListFolders added in v0.2.0

func (q *Queries) ListFolders(ctx context.Context, workspaceID uuid.UUID) ([]ListFoldersRow, error)

Every folder in the workspace, with how many links are filed directly in it.

Flat and unordered by structure: the caller builds the tree. Sorted by name so that the assembled tree's sibling order is stable, and by id after it so two names that fold to the same string under a collation cannot swap between page loads.

The count is a grouped scan of links_folder_idx rather than a correlated subquery per folder, so a workspace with many folders costs one pass instead of one index probe each. It counts links filed *directly* here — a parent does not report its children's links, because the number beside a folder has to mean the same thing as the number of rows the list shows when you click it.

func (*Queries) ListInstanceAuditLogs added in v0.2.0

func (q *Queries) ListInstanceAuditLogs(ctx context.Context, arg ListInstanceAuditLogsParams) ([]AuditLog, error)

The instance-wide audit surface (F36, D98). Rows with no organization at all: an act that changed every tenant and belongs to none of them.

A separate statement rather than a predicate bolted onto the one above, for two reasons that point the same way. The query above rides audit_logs_org_time_idx as a range scan; an OR reaching NULL organizations would turn it into a bitmap scan and a sort on a table designed to grow forever. And the surface is genuinely separate: it is read by the instance principal under audit.read.instance, not by whoever happens to hold audit.read in some organization, so merging the two would mean deciding per row which permission had authorized it.

Same keyset shape, so a client that paginates the organization log paginates this one.

func (*Queries) ListInstanceGrantHolders added in v0.2.0

func (q *Queries) ListInstanceGrantHolders(ctx context.Context, permission string) ([]ListInstanceGrantHoldersRow, error)

Who holds one instance-level permission, with enough of the account to name them on the page that confers it.

Soft-deleted accounts are filtered rather than shown as inert: a grant to an account that cannot authenticate is not reach, and listing it invites somebody to revoke a row that was already doing nothing.

func (*Queries) ListInstanceGrants added in v0.2.0

func (q *Queries) ListInstanceGrants(ctx context.Context, userID uuid.UUID) ([]string, error)

Instance-level grants: what a person may do to the instance rather than to a tenant (D98). Every statement here joins `permissions` on the slug instead of taking a permission id, so the slugs stay the vocabulary the Go code speaks and no caller has to carry a uuid literal around. Every instance-level permission one person holds.

Read on every identity resolution, beside GetUserPermissions, which is why it is keyed on the user alone and returns slugs: the caller folds it into the same set and Identity.Can cannot tell the two sources apart. It deliberately does not join workspaces or memberships — an instance grant is not reached through a tenancy, and an account that belongs to no organization at all (D36) keeps whatever it holds here.

func (*Queries) ListInvitations added in v0.2.0

func (q *Queries) ListInvitations(ctx context.Context, organizationID uuid.UUID) ([]ListInvitationsRow, error)

The administrator's list, newest first.

No pagination and no cursor. An organization's outstanding invitations are a handful of rows by construction, and redeemed ones stop accumulating the moment people join; the link list's machinery here would be a page that cannot fill.

The inviter is joined as a label rather than an id, for the same reason the audit log stores one: the row has to stay readable after that account is gone, and the LEFT JOIN is what lets it.

func (*Queries) ListLinkDestinations added in v0.2.0

func (q *Queries) ListLinkDestinations(ctx context.Context, arg ListLinkDestinationsParams) ([]ListLinkDestinationsRow, error)

Every destination a link has, for the per-destination breakdown to name.

Includes the link's own destination at position 0, because a click recorded before the link had a split — or on a link whose split was later removed — carries a NULL destination_id and is attributed to exactly that row. A breakdown that could not name it would be a chart with an unlabelled bar holding most of the traffic.

func (q *Queries) ListLinks(ctx context.Context, arg ListLinksParams) ([]ListLinksRow, error)

Keyset pagination over (created_at, id).

The cursor is a composite so ordering is total: created_at alone is not unique, and a tie at the page boundary would drop or duplicate rows. Comparing the pair with row-value syntax lets the composite index serve it directly.

Sorting is a CASE rather than three separate queries because sqlc has no dynamic SQL. If plan stability becomes a problem this splits into ListLinksNewest/Oldest/Clicks; measure before doing that. The two tag aggregates are paired positionally by the caller, so they must agree on their order — and on the table they read. Aggregating names from a join and ids from link_tags alone, each sorted by its own column, produced arrays in different orders whenever a link's tags sorted differently by name than by id, and every tag came back carrying another tag's name. One subquery, one ORDER BY, both columns.

func (*Queries) ListMembers added in v0.2.0

func (q *Queries) ListMembers(ctx context.Context, organizationID uuid.UUID) ([]ListMembersRow, error)

Membership management: who is in an organization, at what rank, and where that rank reaches (M28).

The rows these statements read and write are the ones 00200 has carried since Phase 1. Nothing here is new schema; what is new is that a person can change them, which is why every write is scoped by organization_id as well as by id. Every membership in an organization, most powerful first.

One row per membership, not per user. A user holding an organization-wide membership and a workspace-scoped one appears twice, and that is the shape the page has to show: under D31 the two rows *add*, and collapsing them into one would hide the second grant behind the first.

The workspace name is joined rather than looked up per row, and is NULL for an organization-wide membership — which is what the absence of a workspace_id means, and the distinction the list is read for.

Not paginated. An organization's membership is a handful of rows by construction, exactly as its invitations are.

func (*Queries) ListMembershipAuthority added in v0.2.0

func (q *Queries) ListMembershipAuthority(ctx context.Context, arg ListMembershipAuthorityParams) ([]ListMembershipAuthorityRow, error)

Every membership one user holds in an organization, with the rank it carries and whether the role behind it grants a named permission.

This is the authorization side of the sentence LockOrganizationOwners states just above: **a workspace-scoped membership grants authority over its own workspace, not over the organization.** The evaluator answers a different question — what may this person do in the workspace they are *acting in* — by taking the union of every matching membership and the lowest rank among them (D31), which is right for an object that lives in a workspace and wrong for one that spans the organization. A workspace-scoped admin resolved into their own workspace otherwise carries rank 20 against an organization-wide membership their membership does not reach at all, and F27 walked exactly that: one dropdown on /members turned them into an organization-wide admin.

So the rows come back unfolded, one per membership, and the caller keeps the ones that reach the scope of the object being written. A membership scoped to a deleted workspace reaches nothing, matching GetUserPermissions.

The permission is a parameter rather than a join in Go because the answer is per role, not per membership: two memberships at the same role give the same answer, and asking the database means the grant is read from role_permissions — the same table the evaluator reads — rather than from a second list of which roles hold what.

func (*Queries) ListNotifications added in v0.2.0

func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]Notification, error)

Newest first, keyset on (created_at, id). Same shape as the audit log and the link list: an offset shifts under a notification arriving mid-page, and a new notification arriving is the normal case here rather than the rare one.

func (*Queries) ListOrganizationIDs added in v0.2.0

func (q *Queries) ListOrganizationIDs(ctx context.Context) ([]uuid.UUID, error)

Every organization on the instance. Phase 1 has exactly one; this is written as a list because M28 makes that untrue and a job that notified only the first organization would then be silently wrong for every other.

func (*Queries) ListPendingDomainsForVerification added in v0.2.0

func (q *Queries) ListPendingDomainsForVerification(ctx context.Context, rowLimit int32) ([]ListPendingDomainsForVerificationRow, error)

Registered hostnames that are not being served, oldest check first.

Included deliberately, and second. Somebody who registers a hostname and publishes the record should not have to come back and press a button — the on-demand check exists for the person who does not want to wait, not because waiting is the only other option. What they may now have to wait for is a pass with room left after the serving class, which is the price of the serving class never waiting for them.

func (*Queries) ListPermissionSlugs

func (q *Queries) ListPermissionSlugs(ctx context.Context) ([]string, error)

The scope vocabulary. Scopes are validated against the permissions table rather than a list in Go, so RBAC and API keys cannot drift apart.

func (*Queries) ListQRCodes added in v0.3.0

func (q *Queries) ListQRCodes(ctx context.Context, arg ListQRCodesParams) ([]ListQRCodesRow, error)

Every code a link carries (M50), in alphabetical order by name.

Unpaginated, and bounded instead by domain.MaxQRCodesPerLink, which is the same trade ListCampaigns makes: the cap is small enough that a page of them is the whole set, and a pager over a list that cannot exceed it would be a control nobody ever operates.

**The default no longer leads, and that reverses what this comment argued** (M50.8). The key was `NOT (is_default OR slug = ”)` — false before true, so the flag-holder led whatever order the rest were created in — and the defence written here was that *"the list re-orders when the reader moves it, which is the visible half of what setting a default does"*. The owner reported the other half of that: *"Selecting a different default code re-orders the list of codes which can make it seem like the selection didn't change."* The argument is answered rather than withdrawn. M50.7 put a filled icon on the row that holds the flag, so being visible is now the icon's job; the sort was carrying it as a side effect, and carrying it was what made the change look like nothing had happened.

`lower(q.label)` with `q.id` behind it, which is exactly `ORDER BY lower(c.name), c.id` at ListCampaigns above: one collation for one product, and a tie-break so two codes sharing a name have a stable order rather than whatever the plan returns. An unnamed code sorts first, which is where the default of a link nobody has named codes on already was.

**Nothing may read position 0 as the default any more.** link.ListQRCodes and analytics.qrCodeSplit both did — `!rows[0].IsDefault && rows[0].Slug != ""` as the test for "no row holds the flag" — and both scan every row since this milestone. The flag is a column; it was never a position.

func (*Queries) ListRoutingRules added in v0.2.0

func (q *Queries) ListRoutingRules(ctx context.Context, arg ListRoutingRulesParams) ([]ListRoutingRulesRow, error)

The management list: every rule on a link, enabled or not, in the order the redirect path would evaluate them.

Ordered by (priority, created_at) rather than by priority alone. Priority is not unique, and two rules that tie have to be evaluated in a defined order or the same request resolves differently on two replicas. Creation order is the tiebreak because it is the only one a person can predict from the list they are looking at.

Workspace-scoped in the WHERE like every other management read, so a rule belonging to another tenant returns no rows rather than a row the caller has to remember to reject.

func (*Queries) ListServingDomainsForVerification added in v0.2.0

func (q *Queries) ListServingDomainsForVerification(ctx context.Context, rowLimit int32) ([]ListServingDomainsForVerificationRow, error)

The re-verification job's work list, in two classes (M40, reopened).

**One queue could be starved, and the thing it starved was the hard stop.** The job walked a single list ordered `verification_checked_at NULLS FIRST`, which is the right order for one class and fatal across both: `RenameDomain` writes that column back to NULL, so a workspace renaming its rows in a loop kept them at the head of the queue for ever, while a *serving* hostname — which always carries a watermark, because a check is what made it serve — sorted last and was never reached. The only mechanism that takes a lapsed or hijacked hostname out of service therefore stopped running instance-wide, while every pass logged healthy counts.

Splitting on `verified_at` closes it exactly, because a rename un-verifies: churn can only ever crowd the *pending* class, and the class whose checks can stop serving is drawn separately and walked first. Neither statement reads a NULL `verification_checked_at` as anything but "not checked yet" — it is what a live renamed row carries, and treating it as abandonment would delete the registration of anybody mid-cut-over. Hostnames this instance is serving, oldest check first.

Walked first and given the whole budget it needs, because these are the rows where a failing check has a consequence: the grace window runs against them and `UnverifyDomain` ends it. A pass that runs out of time must run out of it on the class where the cost of waiting is another hour of not-yet-serving, not on the class where it is another hour of serving a name whose DNS is gone.

func (*Queries) ListTags

func (q *Queries) ListTags(ctx context.Context, workspaceID uuid.UUID) ([]ListTagsRow, error)

Counts l.id, not lt.link_id. The join onto links is what excludes trashed links, but counting the link_tags column ignored it: a LEFT JOIN keeps the link_tags row when its link is soft-deleted, so the count included trashed links for the whole 30-day window and the tag list disagreed with the link list it filters.

func (*Queries) ListUnreadNotificationPreview added in v0.2.0

func (q *Queries) ListUnreadNotificationPreview(ctx context.Context, arg ListUnreadNotificationPreviewParams) ([]ListUnreadNotificationPreviewRow, error)

The whole of the header's notification lookup: the newest unread rows the bell previews, and the unread total the badge shows, in one round trip.

Two shapes already in this file, composed rather than a third one. The predicate is CountUnreadNotifications' predicate character for character, so notifications_user_unread_idx still serves it; the ordering is ListNotifications' ordering, so the preview is the same "newest first" the page shows.

`count(*) OVER ()` is what makes it one query instead of two. Window functions are evaluated before LIMIT, so the count is every unread row rather than the handful returned — which is the only reason the badge can keep being exact while the preview stays bounded. A page render costs one notification query here, as it did when it cost a bare count.

func (*Queries) ListUserSessions

func (q *Queries) ListUserSessions(ctx context.Context, userID uuid.UUID) ([]ListUserSessionsRow, error)

func (*Queries) ListUsers

func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error)

func (*Queries) ListUsersWithRoleInOrg added in v0.2.0

func (q *Queries) ListUsersWithRoleInOrg(ctx context.Context, arg ListUsersWithRoleInOrgParams) ([]ListUsersWithRoleInOrgRow, error)

Who to tell about something that concerns the organization rather than a person. Active users only: a deactivated account cannot sign in to read it.

**Scoped, because a membership is** (D44). The sentence LockOrganizationOwners states in members.sql applies here word for word: a workspace-scoped owner membership grants ownership of one workspace, not of the organization. So the recipients are the organization-wide rows, plus — when the news belongs to a workspace — the rows scoped to *that* workspace, because news about their workspace is theirs to hear.

Two arms rather than one predicate, and that is the whole correction. Adding `m.workspace_id IS NULL` alone is the smaller diff and the wrong recipient set: it silences a workspace-scoped owner about their own workspace, which is exactly what a caller passing a workspace id means to tell them about. A NULL @workspace_id is news that belongs to no workspace, and the second arm matches nothing then, which is what the organization-wide callers want.

r.organization_id IS NULL for the reason LockOrganizationOwners carries it: 'owner' names a built-in role, and a tenant's custom role of the same slug is a different role.

DISTINCT because the arms overlap. One person may hold both an organization-wide owner membership and an owner membership scoped to this workspace — that pair is precisely how the defect was reachable — and a recipient list naming them twice would write two inbox rows and send two mails.

The address comes back with the id because both deliveries address the same person: the inbox row is keyed by user, the mail by address, and looking the second one up separately would mean a query per recipient.

func (*Queries) ListVariantRules added in v0.2.0

func (q *Queries) ListVariantRules(ctx context.Context, arg ListVariantRulesParams) ([]ListVariantRulesRow, error)

A link's whole split, including the disabled arms and the fallback, in rotation order.

Ordered by the destination's position rather than by created_at: position is assigned once, from NextRuleDestinationPosition, and is the only ordering that a person reading the list can predict and that a rotation can be explained against. Two arms created in the same transaction cannot tie on it.

func (*Queries) ListVerifiedDomains added in v0.2.0

func (q *Queries) ListVerifiedDomains(ctx context.Context) ([]ListVerifiedDomainsRow, error)

Verification and serving (M40).

The gate this milestone exists for is one column: `verified_at`. Nothing below lets a hostname be served without it, and the two statements that clear it — UnverifyDomain here, RenameDomain above — are the only ways serving stops. Everything the host router may resolve aliases on, which is the whole of what the in-process hostname cache holds.

**`verified_at IS NOT NULL` is the gate.** A registered-but-unverified hostname is absent from this result, so it is absent from the cache, so the router has nothing to match a Host header against and the request lands on ops-only 404. There is no second predicate anywhere that could disagree with this one, because there is no second query.

The instance default is excluded: it is matched on `is_default` at boot and serves through the ordinary link host, and including it here would give one hostname two routes into the redirect tree.

Hostnames are lowered here as well as at lookup. `HostCache.Reload` runs each one through `config.HostOnly`, which is what a Host header is spelled with, so the two sides cannot disagree even if this SELECT stops lowering.

func (*Queries) ListWebhookDeliveries added in v0.2.0

func (q *Queries) ListWebhookDeliveries(ctx context.Context, arg ListWebhookDeliveriesParams) ([]ListWebhookDeliveriesRow, error)

One webhook's recent attempts, newest first, for the panel and the API. Bounded by the caller: this is a log, and a page that renders all of it renders a log.

func (*Queries) ListWebhooks added in v0.2.0

func (q *Queries) ListWebhooks(ctx context.Context, workspaceID uuid.UUID) ([]ListWebhooksRow, error)

The secret is not selected. It is written once and read only by the signer, so nothing that renders a page or answers the API can leak it by accident.

func (*Queries) ListWorkspacesForUser added in v0.2.0

func (q *Queries) ListWorkspacesForUser(ctx context.Context, userID uuid.UUID) ([]ListWorkspacesForUserRow, error)

The workspace switcher: what a user may act in, and what they have chosen.

Resolution itself is in auth.sql, because it is identity, not a feature. These are the reads and writes the switcher and its account setting need. Every workspace a user may act in, with the organization it belongs to.

DISTINCT because a user can hold both an organization-wide membership and a workspace-scoped one in the same organization, which the unique index permits; without it the switcher would list a workspace twice.

is_default is carried here rather than fetched separately so a page render costs one query: the nav switcher needs the list, the account setting needs the list plus which entry is pinned, and neither should cost two round trips.

func (*Queries) LockFirstUserSetup

func (q *Queries) LockFirstUserSetup(ctx context.Context) error

Serializes the setup flow's count-then-create.

Both setup surfaces read CountUsers and then, in a separate transaction, register the first user. Nothing held the gap, and the gap is wide: the argon2 hash runs for ~100ms before the transaction even begins. On a fresh closed instance, setup is unauthenticated and only login-rate-limited, so an attacker polling it could have their CountUsers land in the window while the real operator was hashing, and both would be created as "the first user" — each with their own organization, on an instance the operator believes only they can reach.

Transaction-scoped, so it releases on commit or rollback with nothing to clean up. The key is the ASCII bytes "lcsetup\0" as a literal, NOT a hash of anything; to inspect it from psql use the value directly:

SELECT pg_advisory_xact_lock(7810213058373316608);
func (q *Queries) LockLink(ctx context.Context, id uuid.UUID) (uuid.UUID, error)

The link row, locked, for a guard whose decision is a count.

A count cannot be locked, so `SELECT count(*)` as a ceiling check is a check-then-act: two writers each read a state the other is changing, and both pass a limit neither would pass alone. This is `LockOrganizations`' pattern at the link level — take the lock on the parent, make the decision inside the transaction that writes, and let the second writer block until the first commits and then re-read what it left behind.

Locking the *link* rather than the rules is what makes it work with nothing to lock in the empty case. Postgres takes `FOR KEY SHARE` on a parent when a row referencing it is inserted, and that conflicts with `FOR UPDATE` — so a locked link cannot acquire a routing rule or a split arm while the guard is deciding, including the first one, where a lock on the rules would have had no rows to take (F67).

Returns the id alone: the caller already has the link, and this exists for its lock rather than for its columns. No workspace parameter, for the same reason — every caller has already been through GetLink, which is workspace-scoped, so adding one here would be a second tenancy check in a statement whose job is serialization.

func (*Queries) LockMFAPendingLogin added in v0.3.0

func (q *Queries) LockMFAPendingLogin(ctx context.Context, tokenHash []byte) (LockMFAPendingLoginRow, error)

The pending login behind a presented token, locked.

Joined to `users` because every consumer needs both halves and the alternative is two round trips with the account's state changing between them. `FOR UPDATE OF p` locks the pending row and not the user row: the user row is locked separately by the paths that write it, and locking it here would serialise every second-factor attempt against every other write to the account.

Nothing is filtered out. Expired, consumed, an account that stopped being active while the prompt was open — each is a refusal the caller makes, and each is the same refusal to whoever is looking at the form. Filtering here would collapse them into not-found, which is the same answer, and would cost the tests their ability to tell the five apart.

func (*Queries) LockOrganizationOwners added in v0.2.0

func (q *Queries) LockOrganizationOwners(ctx context.Context, organizationID uuid.UUID) ([]uuid.UUID, error)

The organization's owner memberships, locked.

Organization-wide only: a workspace-scoped owner membership grants ownership of one workspace, not of the organization, so counting it would let the last real owner be removed while a workspace-scoped row stood in for them.

Rows rather than a count, because a count cannot be locked. Taken before any removal or demotion of an owner, so two concurrent administrators cannot each observe two owners and each remove one.

func (*Queries) LockOrganizationWorkspaces added in v0.2.0

func (q *Queries) LockOrganizationWorkspaces(ctx context.Context, organizationID uuid.UUID) ([]uuid.UUID, error)

The organization's workspaces, locked, so no link can be created in one while the link guard below is counting.

Ordered for the same deadlock reason as LockOrganizations.

func (*Queries) LockOrganizations added in v0.2.0

func (q *Queries) LockOrganizations(ctx context.Context) ([]uuid.UUID, error)

Organization teardown (M28.5).

Every statement here is either a guard or the delete it guards. The delete is one line; the guards are the milestone.

**Locking, and why each of these returns rows rather than a count.** A count cannot be locked, so a guard written as `SELECT count(*)` is a check-then-act: two administrators acting at once each read a state the other is changing. The pattern is `LockOrganizationOwners`' — select the rows the decision is made on `FOR UPDATE`, count them in Go, and let the second transaction block until the first commits and then re-read what it left behind. Postgres also gives this a second effect that is the point of it here: inserting a row that references a locked parent takes `FOR KEY SHARE` on that parent, which conflicts with `FOR UPDATE` — so a locked organization cannot acquire a new workspace, and a locked workspace cannot acquire a new link, while the guard is deciding. Every live organization on the instance, locked, for the refusal that stops the last one being deleted.

Instance-wide rather than scoped, because that is what the rule is about: an instance with no organization has no path back that does not involve SQL, the same argument that refuses the last owner and the last workspace.

`ORDER BY id` is load-bearing rather than cosmetic. Two concurrent deletions of different organizations both take this lock, and taking a set of row locks in a different order in each transaction is a deadlock; a fixed order makes it a wait instead. It is also why this runs *before* the target row is read — the target is inside this set, so it is already locked by the time anything else touches it.

func (*Queries) LockOrganizationsSolelyOwnedBy added in v0.3.0

func (q *Queries) LockOrganizationsSolelyOwnedBy(ctx context.Context, userID uuid.UUID) ([]LockOrganizationsSolelyOwnedByRow, error)

The organizations this account owns alone, locked before they are counted.

The refusal M28.5 makes from the other side. `team.guardOwnerSet` blocks removing or demoting an organization's last owner; this blocks deleting the *account* that is one, because otherwise the rule is bypassable by leaving through a different door.

**Organization-wide owner memberships only**, the sentence `LockOrganizationOwners` states and for the same reason: a workspace-scoped owner membership is ownership of one workspace, so counting it would either hide a sole owner or invent one.

The owner rows are locked before the count is taken, so this is a rule rather than a check-then-act — a second administrator promoting or removing an owner blocks until this transaction ends. Ordered by membership id inside the locking CTE so two accounts deleting themselves at once take the rows in the same order and cannot deadlock. MATERIALIZED because the lock has to be taken once, on the rows this counts, rather than folded into the outer query by the planner.

func (*Queries) LockUserForDeletion added in v0.3.0

func (q *Queries) LockUserForDeletion(ctx context.Context, id uuid.UUID) (User, error)

Account deletion and subject erasure (M52).

Two operations that look like one and are deliberately kept apart. **Deletion** is interactive, immediate and one transaction: it ends every route into the account and releases the address. **Erasure** is the hourly sweep that scrubs what deletion could not reach — every record *about* this person that ending the account does not remove. *No foreign key to `users`* was that criterion while the sweep touched two tables and is not the criterion now: it reaches four, two of which do carry one. The enumeration lives on `EraseDeletedAccounts` below, beside the statement, and nowhere else.

The statements below are the whole of the database side. `04000` adds the one index the sweep reads and nothing else; the columns have existed, unwritten, since `00200_identity.sql`. The account being deleted, locked for the rest of the transaction.

`deleted_at IS NULL` is in the predicate rather than checked afterwards, so a second deletion of the same account is not-found instead of a second pass over rows the first one already took. Two browsers pressing the button at once is the ordinary way that happens.

func (*Queries) LockUserMFA added in v0.3.0

func (q *Queries) LockUserMFA(ctx context.Context, id uuid.UUID) (LockUserMFARow, error)

The same row, locked for the rest of the transaction.

Every write below reads through this first. Enrolment, disabling and accepting a code all read a column and then write it, and two of those are the difference between a second factor existing and not — a check-then-act on `mfa_enabled_at` is how an abandoned enrolment and a live one end up in the same account.

func (*Queries) LockWorkspaceFolders added in v0.2.0

func (q *Queries) LockWorkspaceFolders(ctx context.Context, workspaceID uuid.UUID) ([]uuid.UUID, error)

Every folder in a workspace, locked, for a decision made over the whole tree.

`MoveFolder`'s refusals are computed in Go from a tree read a moment earlier — *is the new parent inside the subtree being moved* cannot be written as a column check — so the read and the write have to be one transaction or two concurrent moves each decide against a tree the other is changing. Moving A under B while B moves under A passes both checks and produces the cycle M38 says can never exist (F108).

The whole workspace rather than the two rows involved, because the predicate is over the whole tree: a cycle can run through folders neither move names. Workspaces are small enough for that to be one indexed read.

`ORDER BY id` is load-bearing rather than cosmetic, and it is `LockOrganizations`' reasoning: two transactions taking the same set of row locks in different orders deadlock, and a fixed order makes it a wait instead.

Returns ids alone. The caller reads the tree it decides on through ListFolders inside the same transaction; this exists for its lock.

func (*Queries) MarkAPIKeyRotated added in v0.2.0

func (q *Queries) MarkAPIKeyRotated(ctx context.Context, arg MarkAPIKeyRotatedParams) (int64, error)

Closes the predecessor: names its successor and sets the far edge of the grace window.

`successor_id IS NULL` in the WHERE clause is belt to the FOR UPDATE braces. The lock is what serializes; this is what makes the second writer a no-op rather than a silent overwrite if the lock is ever dropped from the read.

func (*Queries) MarkAllNotificationsRead added in v0.2.0

func (q *Queries) MarkAllNotificationsRead(ctx context.Context, userID uuid.UUID) (int64, error)

func (*Queries) MarkAutomationRulesChecked added in v0.2.0

func (q *Queries) MarkAutomationRulesChecked(ctx context.Context, arg MarkAutomationRulesCheckedParams) error

The cursor advance, for the rules one run actually looked at.

One statement per run rather than one per rule, so the whole of the fairness mechanism costs a single indexed update however many rules were considered — the per-run bound m43.md asks for is a product of four constants plus this.

**It writes `last_checked_at` and nothing else.** Not `last_fired_at`: a rule that matched nothing, or matched less than its threshold, has not fired, and moving its watermark would discard the subjects already inside its window. Not `updated_at` either — this is the scheduler's bookkeeping rather than an edit somebody made, and touching it would make every rule on the instance look edited once a minute.

Rules whose evaluation *failed* are in the list on purpose. A rule with a corrupt `actions` column errors on every pass; leaving its cursor where it was would park it at the head of the queue forever, which is F83 again with a different cause.

func (*Queries) MarkDefaultQRCode added in v0.3.0

func (q *Queries) MarkDefaultQRCode(ctx context.Context, arg MarkDefaultQRCodeParams) (int64, error)

The second half, and never run on its own: without the clear before it, it is the collision the comment above describes.

func (*Queries) MarkDomainTLSActive added in v0.2.0

func (q *Queries) MarkDomainTLSActive(ctx context.Context, id uuid.UUID) (int64, error)

Caddy asked whether to obtain a certificate for this hostname and was told yes. Guarded on 'pending' so it is one write per verification rather than one per handshake: the ask endpoint is public and unauthenticated, and a statement it could run on every request would be a write amplifier anybody can pull.

func (*Queries) MarkDomainVerificationFailed added in v0.2.0

func (q *Queries) MarkDomainVerificationFailed(ctx context.Context, arg MarkDomainVerificationFailedParams) (Domain, error)

A failed check, and deliberately *not* a stop.

verified_at is untouched: a domain that is serving goes on serving while the grace window runs (D70). What this records is that the window has started — COALESCE keeps the first failure's timestamp, so a run of failures anchors on when the run began rather than sliding forward with every poll, which would make the window unreachable.

**The hostname and the token are in the predicate for the reason MarkDomainVerified carries them (M40, reopened; F131).** This is the same read-check-write, over the same gap held open by the same nameserver, and the rename that commits inside it is the same rename. What a late write lands here is smaller than what it lands there and it is not nothing: a `verification_error` sentence naming the *old* hostname, shown on the Domains page against the new one, and a `verification_checked_at` the new name never earned — which moves the row out of the head of the pending queue that `verificationWorkList` orders NULLs first, so the name nobody has checked is checked later than it should be. Predicating on what was proved makes the late write affect zero rows, and the caller treats that as the conflict it is.

It also closes the statement below transitively on the job's path. A failed check that finds no row to land on returns before UnverifyDomain is reached, so the grace window cannot expire against a hostname this pass never checked — UnverifyDomain's own `verified_at IS NOT NULL` was already refusing that write, and now nothing gets that far to be refused.

func (*Queries) MarkDomainVerified added in v0.2.0

func (q *Queries) MarkDomainVerified(ctx context.Context, arg MarkDomainVerifiedParams) (Domain, error)

A successful check, written only onto the row that was actually checked.

**The hostname and the token are in the predicate because they are what was proved (M40, reopened).** Verification reads a row, resolves DNS against the hostname it read — seconds, against a nameserver the registrant runs — and then writes here. `RenameDomain` is concurrently reachable and clears `verified_at`; an unconditional write landing after it would fill exactly that NULL through the COALESCE below and start serving a name nobody proved, up to and including one of the instance's own hosts. Predicating on `hostname` and `verification_token` makes the late write affect **zero rows** instead, and the caller treats that as the conflict it is.

A transaction around the read and the write would not have closed this: the rename commits in the gap between two separate transactions, so there is nothing for it to serialise against. `FOR UPDATE` across the lookup would have, by pinning a row lock for the DNS timeout inside a job that walks a batch — which is a different outage.

Sets verified_at only when it is not already set, so a domain that has been serving for a month does not have its start date rewritten every hour — the column answers "since when has this been served", and a re-check is not a new answer.

ssl_status moves to 'pending': the app never speaks ACME (decision D3), so the most it can say is that it will now answer Caddy's on-demand ask for this hostname. 'active' is written by that ask endpoint, once, when it is first consulted.

The failing streak is cleared unconditionally, which is D70's "a successful check at any point resets the count".

func (*Queries) MarkInvitationRedeemed added in v0.2.0

func (q *Queries) MarkInvitationRedeemed(ctx context.Context, arg MarkInvitationRedeemedParams) (int64, error)

The single-use write. Conditional on the invite still being redeemable, so even without the lock above this could not be spent twice.

func (*Queries) MarkMailFailed added in v0.2.0

func (q *Queries) MarkMailFailed(ctx context.Context, arg MarkMailFailedParams) error

Retry exhausted. Terminal, and deliberately not deleted — a row that says what was attempted and why it never arrived is the whole point of an outbox over an in-memory retry loop.

Blanked for F32's reason, and the failed case is the one that needs it most: a message that never arrived is one whose token is still unspent, and this row would otherwise hold it for thirty days against an invitation that lives seven. The retry path deliberately does not blank — a row still being retried is pending, and it has to keep the message it is going to send.

func (*Queries) MarkMailRetry added in v0.2.0

func (q *Queries) MarkMailRetry(ctx context.Context, arg MarkMailRetryParams) error

A failure that will be tried again. The error is kept verbatim: it is what an operator reads when somebody reports that mail never arrived.

Replaces the lease ClaimDueMail set with the real backoff for this attempt, and does not touch attempts — the claim already spent it.

Seconds rather than an interval parameter, matching the lockout query in auth.sql: an interval maps to pgtype.Interval, which would put a driver type in the service layer's signature for no benefit.

func (*Queries) MarkMailSent added in v0.2.0

func (q *Queries) MarkMailSent(ctx context.Context, id uuid.UUID) error

attempts is not touched: ClaimDueMail already spent it.

The body is blanked here, in the same statement that marks the row sent (finding F32). Two of the templates this phase ships carry a single-use token in their body, and a delivered message is one whose token has reached the only person entitled to it — keeping a copy afterwards is keeping a redeemable credential in clear for the retention window. Folded into this UPDATE rather than done after it, so there is no crash window in which a row is sent and still carries the token, and so `mail_outbox_finished_body_scrubbed` cannot be passed by a caller that forgets. What an operator reads afterwards — recipient, subject, kind, attempts, last_error — is untouched.

func (*Queries) MarkNotificationRead added in v0.2.0

func (q *Queries) MarkNotificationRead(ctx context.Context, arg MarkNotificationReadParams) (int64, error)

Scoped by user_id as well as id, so someone else's notification is a zero-row update rather than a 403 that confirms the id exists.

read_at is only set once. Marking an already-read notification is a no-op rather than a fresh timestamp, so "when did you first see this" survives a double click.

func (*Queries) MarkNotificationUnread added in v0.3.0

func (q *Queries) MarkNotificationUnread(ctx context.Context, arg MarkNotificationUnreadParams) (int64, error)

`read_at` back to NULL, which is the whole of "unread": 00600 declared the column nullable and the inbox has always used NULL for it, so putting one back is an UPDATE and never a migration (M48).

**Deliberately not the mirror image of MarkNotificationRead.** That statement refuses to touch an already-read row so that "when did you first see this" survives a double click. This one carries no such guard: it exists because the click-through M48 adds marks a notification read as a side effect of opening it, and somebody undoing that is saying they have not dealt with it — which is as true of a row read last week as of one read by accident a second ago. The first-seen timestamp is what is being discarded, on purpose, by the person it belongs to.

func (*Queries) MarkWebhookAbandoned added in v0.2.0

func (q *Queries) MarkWebhookAbandoned(ctx context.Context, arg MarkWebhookAbandonedParams) error

Retry exhausted. Terminal, and deliberately not deleted: a row saying what was attempted, how many times, and what the receiver said is the whole reason this is a table rather than an in-memory retry loop.

func (*Queries) MarkWebhookDelivered added in v0.2.0

func (q *Queries) MarkWebhookDelivered(ctx context.Context, arg MarkWebhookDeliveredParams) error

attempts is not touched: the claim already spent it.

func (*Queries) MarkWebhookRetry added in v0.2.0

func (q *Queries) MarkWebhookRetry(ctx context.Context, arg MarkWebhookRetryParams) error

A failure that will be tried again. Replaces the lease the claim set with the real backoff for this attempt, and does not touch attempts.

response_code is nullable and stays NULL when there was no response at all, which is what tells a refused connection apart from a receiver answering 500.

func (*Queries) MatchBlockedDestination added in v0.2.0

func (q *Queries) MatchBlockedDestination(ctx context.Context, candidates []string) (MatchBlockedDestinationRow, error)

The low-confidence destination blocklist (M30).

Read on the management path only. The redirect tree never touches this table: blocking decides what may be stored, not what may be served, and putting a query here on the hot path would buy nothing a link that was refused at creation does not already have. Whether any of a host's label-boundary suffixes is on the list.

The caller passes the full host and every parent of it — a.b.example becomes {a.b.example, b.example, example} — so the label-boundary rule is enforced by what is asked for rather than by a pattern match, and the whole question is one index probe. Longest first in the caller, and ORDER BY length here, so a specific entry wins over the parent it sits under and the reason an operator reads is the one they wrote for that host.

The source comes back because it decides which rule the refusal reports: a seeded shortener says shortener_chain and everything else says operator_blocklist. One row rather than one query per source — a host that is both listed by the operator and a known shortener is one refusal, and the more specific entry is the longer one, which this already returns.

func (*Queries) MatchExhaustedBudgets added in v0.2.0

func (q *Queries) MatchExhaustedBudgets(ctx context.Context, arg MatchExhaustedBudgetsParams) ([]MatchExhaustedBudgetsRow, error)

Links whose durable click budget ran out inside the window (M35).

`link_click_budget.exhausted_at` is stamped in the same transaction that spends the last click, so it is an exact event time. `links.click_count` is not, and 02100 says out loud that it must never be an authorization input; it is not one here either.

Walks link_click_budget_exhausted_idx (02100) — declared DESC, read forward, which Postgres serves from the same index backwards.

The lower bound is the composite watermark, in the same range-plus-tiebreak spelling MatchExpiredLinks explains. Ties on `exhausted_at` are rarer here than on expiry — each stamp is its own transaction — but two budgets spent in the same microsecond are not impossible, and the cursor costs nothing.

func (q *Queries) MatchExpiredLinks(ctx context.Context, arg MatchExpiredLinksParams) ([]MatchExpiredLinksRow, error)

Links whose expiry fell inside this rule's window.

**No status filter, on purpose.** `archive_link` is one of the actions a rule may take, so a trigger that excluded archived links would shrink its own match set when it fired — a rule feeding off its own effect, which is exactly what domain/automation.go's TriggerReads/ActionWrites declaration says cannot happen. `links_expiry_idx` (00300) is unusable here for that reason: its predicate carries `status = 'active'`. 02900 adds automation_links_expiry_idx, which does not.

Soft-deleted links are excluded, and that exclusion is safe for the opposite reason: nothing an automation does writes `deleted_at`.

The lower bound is the composite watermark, and its spelling is deliberate: `expires_at >= @after` plus a tiebreak is the same predicate as `(expires_at, id) > (@after, @after_subject)`, written so the planner keeps a single range condition to walk automation_links_expiry_idx with and applies the tiebreak as a filter on the handful of boundary rows. Strict `>` on the timestamp alone is the shape this replaces, and it was lossy: a capped fetch that stopped inside a group of links sharing one expiry — bulk creation makes those routine — left the tied remainder outside every later window, forever.

func (*Queries) MatchWorkspaceAuditEvents added in v0.2.0

func (q *Queries) MatchWorkspaceAuditEvents(ctx context.Context, arg MatchWorkspaceAuditEventsParams) ([]MatchWorkspaceAuditEventsRow, error)

Administrative events of one action inside the window.

The blocked-attempt trigger's source, and it reads the audit log because that is the only durable trace a refusal leaves — `blocked_destinations` (01500) is the operator's blocklist, not a record of attempts against it.

The action is a parameter with exactly one caller, which passes domain.TriggerDestinationBlocked. The three names for that event — the trigger, the audit action and the webhook event — are the same string, and TestTheBlockedVocabularyIsOneWord holds them together.

Walks audit_logs_workspace_action_idx (02900).

The lower bound is the composite watermark, in the same range-plus-tiebreak spelling MatchExpiredLinks explains. `occurred_at` carries whatever instant the recorder was handed, so a burst of refusals in one transaction shares a timestamp the way bulk-created links share an expiry.

func (*Queries) MoveFolder added in v0.2.0

func (q *Queries) MoveFolder(ctx context.Context, arg MoveFolderParams) (Folder, error)

The parent is set outright rather than COALESCEd, because NULL is a destination here: it means the root. A partial-update idiom would make "move to the top level" unexpressible.

func (*Queries) NameQRCode added in v0.3.0

func (q *Queries) NameQRCode(ctx context.Context, arg NameQRCodeParams) (int64, error)

Gives a slug to the one code that may not have one (M50's reopening, D183).

A link's only code carries no slug: there is nothing to tell it apart from, and handing one out while writing a style would change what a picture says. When a second code appears the first one needs a tag, and this is the statement that writes it.

**`AND slug = ”` is what makes it structurally incapable of a rename.** A slug is printed, so moving one breaks every copy already in the world — UpdateQRCodeLabel says so above and this is the same rule enforced by the WHERE clause rather than by the caller. Naming a code that has no name is not moving anything: nothing printed carries the value being replaced, because there was no value.

**`is_default = true` goes with the slug, and it is the one statement in this file that sets the flag without clearing another.** The row this reaches is whichever row GetDefaultQRCode answered with, and that read falls back to the empty slug for a row the flag never reached — one written by the previous release during a rolling deploy, where `is_default` is a column it does not know about. Taking the empty slug off such a row without putting the flag on it would leave the link matching neither half of `(is_default OR slug = ”)`: no default at all, a phantom code synthesised into every list and breakdown, and the untagged `qr` bucket no longer folding onto the code every already- printed picture of this link resolves through. The empty slug and the flag are two spellings of the same fact, so the statement that removes one writes the other.

Against `qr_codes_link_default_key` this is safe by the same read: it runs only on a row carrying the empty slug, and GetDefaultQRCode orders the flag first, so a link with some *other* row flagged never returns this row to be named. What the read cannot rule out is the flag moving between it and this write, which is a unique violation and is the caller's to answer — CreateQRCode re-reads the winner rather than failing over it.

func (*Queries) NextRuleDestinationPosition added in v0.2.0

func (q *Queries) NextRuleDestinationPosition(ctx context.Context, linkID uuid.UUID) (int32, error)

The next free position above the primary. COALESCE so the first rule on a link lands at 1 rather than at NULL.

func (*Queries) NextVariantRotation added in v0.2.0

func (q *Queries) NextVariantRotation(ctx context.Context, arg NextVariantRotationParams) (int64, error)

Advance a link's sequential rotation and return the position it advanced to (M36, D8).

The same table, the same upsert shape and the same concurrency argument as ConsumeClickBudget: the statement is the transaction, two replicas serialise on the row lock the ON CONFLICT path takes, and the loser is evaluated against the winner's committed value. That is what makes the order strict *globally* rather than per process — an in-memory counter would give each replica its own rotation and "sequential" would mean "sequential here", which is a support ticket rather than a feature.

A different column from `consumed` on purpose. A rotation advances; a budget is spent and refuses when it runs out. Sharing one number would let a sequential arm consume a one-time link's single click on its way to being chosen, and the gate that runs afterwards would find the link already spent.

Unconditional: there is no limit to reach, so unlike ConsumeClickBudget this always returns a row. The write lands only on links that actually carry a sequential arm, which is the cost D8 accepts and the reason every other link keeps the unchanged fast path.

func (*Queries) OldestQRCode added in v0.3.0

func (q *Queries) OldestQRCode(ctx context.Context, arg OldestQRCodeParams) (OldestQRCodeRow, error)

The code that has existed longest, which is what a removed default promotes to (M50's reopening).

`created_at, id` is the order ResolveAliasForRedirect enumerates codes in, and the oldest code is the one whose pictures have been in the world longest — which is what makes it the right one to inherit every untagged scan.

**The rule is unchanged and the sentence that explained it is** (M50.8). This comment used to add *"so the promoted code is the one at the top of the list the reader is looking at"*, which was true while ListQRCodes ordered by creation. That list is alphabetical now, so the promoted code is wherever its name puts it. D183 chose the oldest and nothing here reverses that; what went is a claim about where the reader's eye lands, which had stopped being true.

Excluding a row by id rather than filtering on `is_default`, because the caller runs this *after* deleting the flag-holder in the same transaction and inside it that row is already gone — the exclusion is what makes the statement correct if it is ever called before one.

func (*Queries) PeekInvitationByTokenHash added in v0.2.0

func (q *Queries) PeekInvitationByTokenHash(ctx context.Context, tokenHash []byte) (PeekInvitationByTokenHashRow, error)

The same lookup without the lock, for rendering the redemption page.

A GET must not take a row lock: the page is served to anybody holding the link, and a locking read there would let a stranger hold a write lock on the row by opening a page.

func (*Queries) PeekVariantRotation added in v0.2.0

func (q *Queries) PeekVariantRotation(ctx context.Context, arg PeekVariantRotationParams) (int64, error)

Read a link's rotation without advancing it (F100).

The read-only twin of NextVariantRotation, and it exists for one caller: HEAD. A link checker or an unfurler probing a sequentially split link used to advance the durable counter on every probe, re-phasing every subsequent visitor's arm — and because HEAD writes no click event, the per-destination breakdown could not show why the arms were uneven.

Returning early on HEAD is *not* the fix: a HEAD would then answer the link's own destination while a GET answers an arm, so a checker would validate a URL no visitor is ever sent to. HEAD has to choose the same arm the next GET would, which is what this reads.

The same shape as Budget, which reads a click allowance without spending it for exactly the same caller and the same reason. No row means no click has landed yet, and the caller treats that as position 1 — the first arm, which is what the first visitor will get.

func (q *Queries) PurgeExpiredLinks(ctx context.Context, batchSize int32) ([]PurgeExpiredLinksRow, error)

The end of the trash window: hard-delete links whose purge_after has passed.

One statement, so the reservation and the deletion cannot be separated by a crash: an alias that ever received traffic is written to reserved_aliases in the same command that removes its row, and ON CONFLICT makes a retried run converge rather than fail. Aliases that never received a click are released — deliberately, per the reserved_aliases rationale: nothing in the wild points at them, so permanent reservation would only bleed the namespace.

SKIP LOCKED so the purge can never block, or be blocked by, a concurrent restore-by-hand of the same row; a skipped row is caught on the next run. Destinations and link_tags follow by ON DELETE CASCADE. click_events rows carry no FK (partitioned) and are dropped by analytics retention instead.

func (*Queries) PurgeExpiredSalts

func (q *Queries) PurgeExpiredSalts(ctx context.Context) (int64, error)

The de-identification step. Once the salt is gone the day's hashes cannot be linked back to an address.

func (*Queries) PurgeFinishedMFAPendingLogins added in v0.3.0

func (q *Queries) PurgeFinishedMFAPendingLogins(ctx context.Context, batch int32) (int64, error)

The sweep. Lapsed rows and spent ones, in bounded batches, from the hourly maintenance pass that already purges finished registrations and password resets.

No retention window, unlike those two. A spent pending login is evidence of nothing — the session it minted is the record, and the audit trail carries the rest — where a spent registration and a spent reset are each the only trace that an address was proven.

func (*Queries) PurgeFinishedMail added in v0.2.0

func (q *Queries) PurgeFinishedMail(ctx context.Context, maxAgeDays int32) (int64, error)

Sent and failed rows past the retention window. The outbox is a record of what was attempted, not an archive: without this the table is the one thing in the schema that grows forever with no window and no metric, which is the shape D5 and M21 exist to avoid repeating.

It is a retention window and not a secrecy control. The rows it deletes lost their bodies when they finished; this is what stops the record of *that* piling up. Lowering it would not shorten any credential's exposure.

func (*Queries) PurgeFinishedPasswordResets added in v0.3.0

func (q *Queries) PurgeFinishedPasswordResets(ctx context.Context, arg PurgeFinishedPasswordResetsParams) (int64, error)

The sweep. Removes tokens nobody used past their expiry, and spent rows past the same short window a spent registration gets.

Both, because neither is a record of anything a reader needs: the audit log carries that the reset happened, and the password itself is the durable evidence. This table is a waiting room, not an archive.

func (*Queries) PurgeFinishedWebhookDeliveries added in v0.2.0

func (q *Queries) PurgeFinishedWebhookDeliveries(ctx context.Context, maxAgeDays int32) (int64, error)

Delivered and abandoned rows past the retention window. The delivery log is a record of what was attempted, not an archive; without this it is a table that grows forever with one row per link write per webhook, which is the shape D5 and M21 exist to stop repeating.

func (*Queries) PurgeLapsedRegistrations added in v0.2.0

func (q *Queries) PurgeLapsedRegistrations(ctx context.Context, keepDays int32) (int64, error)

The sweep. Removes registrations nobody completed, and consumed rows whose account has long since been created.

Both, because neither is a record of anything: an account that exists is evidence enough that its address was proven, and the audit log carries what happened. This table is a waiting room, not an archive — the one shape it must not have is the unbounded growth D5 and M21 exist to stop repeating.

func (*Queries) RecordFailedLogin

func (q *Queries) RecordFailedLogin(ctx context.Context, arg RecordFailedLoginParams) (RecordFailedLoginRow, error)

Returns the new count so the caller can apply the lockout policy without a second round trip and without a read-modify-write race between two concurrent attempts.

An elapsed lockout starts the count over. Incrementing unconditionally meant the counter only ever went down on a successful sign-in or a password change, so once an account had been locked it sat at the threshold forever: the user waited out the window, got one attempt, and a single wrong guess re-locked them for the full duration. The lockout became permanent for anyone who could not remember their password on the first try — which is the population it applies to.

func (*Queries) RecordJobFailure

func (q *Queries) RecordJobFailure(ctx context.Context, arg RecordJobFailureParams) error

Keeps the watermark where it was: a failed run has not covered its window, and advancing past it would turn one bad run into permanent gaps. Keeps last_success_at where it was for the same reason — the last success is a fact about the past that a later failure does not change, and it is what the staleness gauge measures against.

func (*Queries) RecordSuccessfulLogin

func (q *Queries) RecordSuccessfulLogin(ctx context.Context, id uuid.UUID) error

func (*Queries) RenameDomain added in v0.2.0

func (q *Queries) RenameDomain(ctx context.Context, arg RenameDomainParams) (Domain, error)

The hostname is the only thing a registration has to change, and it is changeable only while nothing serves it; see decisions.md, D69.

Not scoped by owner. The caller has already been judged against the row read by GetDomainByID, and repeating the predicate here would turn a 403 into a 404 for anybody who got past that check by a route this file cannot see.

**A rename un-verifies (M40), and that is the bullet D69 deferred to here.** The proof of control is a TXT record published under the *old* name and says nothing about the new one, so carrying verified_at across would let a workspace verify a name it controls and then rename the row to one it does not. The token is minted afresh for the same reason: the old value is published in somebody else's zone.

func (*Queries) RenameFolder added in v0.2.0

func (q *Queries) RenameFolder(ctx context.Context, arg RenameFolderParams) (Folder, error)

func (*Queries) RenameWorkspace added in v0.2.0

func (q *Queries) RenameWorkspace(ctx context.Context, arg RenameWorkspaceParams) (Workspace, error)

Name and slug move together. The slug is derived from the name by the caller rather than kept as a separate field somebody can edit into disagreement with it, and the partial unique index on (organization_id, lower(slug)) is what refuses a collision.

func (*Queries) ReserveAlias

func (q *Queries) ReserveAlias(ctx context.Context, arg ReserveAliasParams) error

Called before purging a link that has clicks. The alias is in the wild — on printed material and in other people's bookmarks — so handing it to a new destination would be a redirect hijack.

func (*Queries) ReserveOrganizationTraffickedAliases added in v0.2.0

func (q *Queries) ReserveOrganizationTraffickedAliases(ctx context.Context, organizationID uuid.UUID) error

The organization-level half of the same reservation, and it exists for the same reason the link guard exists one level up: an organization-level cascade that behaved differently from a workspace-level one would make the rule bypassable by deleting one level up.

The rationale is written out in full beside ReserveWorkspaceTraffickedAliases; only the reach differs. Note what is deliberately *not* filtered here: workspaces are joined without `deleted_at IS NULL`, unlike CountOrganizationLinks, because this statement follows what the cascade takes rather than what the guard counted, and the cascade takes every workspace of the organization.

func (*Queries) ReserveWorkspaceTraffickedAliases added in v0.2.0

func (q *Queries) ReserveWorkspaceTraffickedAliases(ctx context.Context, workspaceID uuid.UUID) error

Run immediately before DeleteWorkspace, in the same transaction, because the cascade below is the third way a link lets go of its alias and it was the one that let go for free (F28).

CountWorkspaceLinks excludes soft-deleted links deliberately — counting them would leave a workspace undeletable until the purge job ran — so a workspace reaching the delete may still hold trashed links, for up to the trash window. The cascade hard-deletes them without the purge job ever seeing them, and `IsAliasTaken` then stops finding the row: an alias that was on printed material yesterday is claimable by anyone on the instance today.

`click_count > 0` is PurgeExpiredLinks' threshold, and the rename path's, and it is the same threshold on purpose: three paths that release an alias must not hold three opinions about what "in the wild" means. Aliases that never received a click are released, per the reserved_aliases rationale.

No deleted_at predicate, and FOR UPDATE, for one reason between them. The guard above has already established there are no live links, so this selects exactly the trashed ones — unless a row stopped being trashed after the count ran, which today takes a hand-written UPDATE because nothing in the product un-trashes a link (`RestoreLink` restores an *archived* one and requires `deleted_at IS NULL`). The statement follows what the cascade will take rather than what the guard counted, so such a row is reserved rather than skipped, and the lock makes it wait rather than slip between the two.

func (*Queries) ResolveAliasForRedirect

func (q *Queries) ResolveAliasForRedirect(ctx context.Context, arg ResolveAliasForRedirectParams) (ResolveAliasForRedirectRow, error)

The redirect hot path.

Everything here runs under a 20ms budget on the dedicated redirect pool. Keep the query set small, index-covered, and free of joins that are not strictly required. Single-row lookup on links_domain_alias_key.

primary_url is read from the denormalized column rather than joined from destinations: the join would double the row fetches on the hottest query in the system to retrieve a value a trigger already keeps in step.

Status and expiry are returned rather than filtered, so the handler can distinguish 404 (unknown or archived) from 410 (expired) and can cache a negative result. Filtering here would make every non-serving state look identical.

The one join this query has, and the rule above is why it reads the way it does. M32.5 needs the domain's bot-blocking settings on the redirect path, and the alternative — a second lookup, or a second cache with its own invalidation — would put either an extra round trip or an extra staleness window on the hottest path in the product. This is neither: `domain_id` is the domains primary key, the table holds one row on every deployment built so far, and the two booleans ride home inside the round trip that was happening anyway. The cached snapshot then carries them, so a cache hit answers the whole question — link policy and domain policy together — without asking anything.

No `d.deleted_at IS NULL` here, deliberately. A soft-deleted domain row still joins, which is exactly the behaviour this query had before the join existed; adding the filter would silently turn every link on such a domain into a 404, which is a change nobody asked this milestone to make.

The lateral is M34's, and it obeys the same rule the domain join does: the routing rules a link carries have to be in the snapshot, and the alternatives are a second query on every cache miss or a second cache with its own invalidation. This is neither. It is an index probe on `routing_rules_link_idx` — partial on `enabled`, keyed on (link_id, priority) — which finds nothing at all for the overwhelming majority of links, because a link with no rules is the default and always will be. Nothing about it runs on a cache *hit*: by then the rules are already inside the snapshot.

The rules come back as one jsonb array, already in evaluation order, because ordering them here is free and ordering them in Go would mean the sort that decides which destination a visitor gets lived somewhere other than the query that reads them. The keys are spelled out rather than short: this is the database's own vocabulary, and the compact spelling the cached snapshot uses is the Go type's business.

M36 widened the lateral to every kind and left it a single probe, which is the property worth protecting: a link's match rules and its split arms live in one table, on one index, and asking for them separately would double the cost of the only lookup a cache miss makes. The ordering carries both vocabularies at once. `rr.kind <> 'match'` sorts false before true, so M34's rules come first and keep their (priority, created_at) order exactly; the arms follow in `dest.position` order, which is what a rotation is explained against and what the dashboard lists. `created_at` remains the last tiebreak so the order is total whatever else ties.

`id` and `weight` are new here. The id is what a click is attributed to — click_events.destination_id — and the weight is the arm's share; both have to be in the snapshot because reading either at request time would be the query this design exists to avoid.

The second lateral is M50's, and it is the same bargain a third time. A link may carry several QR codes, each printing a slug in its payload, and a scan may only be attributed to a slug this link actually has — otherwise the parameter is an open write surface into `link_dimension_daily`, reachable by anybody who can read a URL. Checking that at request time would be a query on the hot path; checking it against the snapshot is a scan of a slice bounded by domain.MaxQRCodesPerLink. So the slugs ride home in the round trip that was happening anyway, on `qr_codes_link_idx` — the index 03700 restored — and find nothing at all for the overwhelming majority of links, because a link with no named codes is the default and always will be.

Slugs only, never labels or styles. The redirect path has no use for either: a label is what a person reads in the dashboard and a style is how the picture is drawn, and putting them in the snapshot would serialize workspace free text into every cached entry for nothing.

**`q.slug <> ”` stays, and what it excludes has shrunk to one row** (D183). It used to leave the whole default code out, because the default *was* the row with no slug. The flag carries that identity now and the row it used to be gains a slug the moment a second code appears beside it, so a link's default rides home like every other code and a payload naming it is matched. What is still left out is a link's *only* code, which keeps the empty slug and the untagged payload — and it is left out because there is nothing to match it with: `Snapshot.CodeSlug` returns before it scans when the parameter is empty or absent, so an empty string in this array could never be compared against anything. Carrying it would serialize a byte no request can reach into every cached entry for the majority of links, and falsify what `Snapshot.Codes` promises about the payload a link with no named codes carries — which is the premise CacheKeyVersion was not bumped on.

**`is_default` deliberately does not.** A request carrying no `qrc` still records the bare `qr` it has always recorded — the value on every row this product has written since M41 — and which code that belongs to is a question the breakdown answers when somebody reads it, from the flag as it stands then. Resolving it here instead would put the answer in `link_dimension_daily`, where moving the flag afterwards could not reach it, and would rewrite what every pre-reopening scan is stored as for no gain a reader can see.

func (*Queries) ResolveDefaultDomain

func (q *Queries) ResolveDefaultDomain(ctx context.Context) (ResolveDefaultDomainRow, error)

Read once at boot and cached. The default domain is matched on the flag rather than on a hostname string, so it never has to agree with LINKCTRL_BASE_URL.

func (*Queries) ResolveDomainByHostname

func (q *Queries) ResolveDomainByHostname(ctx context.Context, lower string) (ResolveDomainByHostnameRow, error)

PHASE 2: custom domains. Present now because the cache key is already host-scoped, so enabling it later needs no key change.

func (*Queries) ResolveOrganizationForAPIKey added in v0.3.0

Which organization an **account-wide** key's request lands in (M54).

One tier above ResolveWorkspaceForUser, and deliberately not a second copy of it. That statement answers "which workspace, given a person and optionally a bound"; this one answers "which tenant", and the answer then becomes M44's organization_id bound so the workspace precedence stays stated exactly once. Feeding it the organization the request resolved to is the whole of M44's parameter surviving a key that has no organization column to feed it from.

The rungs are the person's, for the reason D90 gives: what an unpinned key follows is the person, and where the person is acting is part of that. Their pinned default's organization wins, then the one they last used, then the oldest they belong to. There is no session rung because there is no session.

**Organization-wide memberships only.** An unpinned key has always required one — GetAPIKeyByPrefix's predicate refuses a workspace-NULL key covered by a workspace-scoped membership, and MayCreateOrgWide refuses to mint one without it. Carrying that rule across the tenancy boundary is what stops an account-wide key widening into an organization where its owner is scoped to a single workspace: a key minted under organization-wide authority must not acquire reach its owner could not have granted it there.

Barred organizations are excluded here as well as in the coarse check, and both matter: the coarse one decides whether the credential authenticates at all, this one decides where it lands, and an administrator who cut their tenant out must not be reachable by a key that simply had nowhere else to go.

**The bar comes back with the answer** (F183). Cutting an organization out of an account-wide key's reach stopped it *acting* there and not *reading about* it: `auth.Service.Workspaces` bounded a key by pinned-or-not, and an account-wide key is by definition not pinned, so the barred organization's name, slug and workspace ids went on being listed to the revoked credential. Closing that needs the whole barred set rather than the one organization this request landed in, and this statement is the only place an account-wide key's reach is already being resolved — so the set rides back with it as an array and the read bound costs no query of its own. `barred` is aggregated in its own CTE so it is computed once rather than per candidate organization, and the exclusion below reads it instead of repeating the subquery: one expression of the fact, which is what a reader of a security predicate should have to check.

func (*Queries) ResolveWorkspaceForUser added in v0.2.0

func (q *Queries) ResolveWorkspaceForUser(ctx context.Context, arg ResolveWorkspaceForUserParams) (Workspace, error)

The workspace a request acts in, and the only place that question is answered. Every identity — session, API key, CLI — comes through here.

The precedence is the ORDER BY and nothing else, so there is one statement of it rather than one per caller:

  1. the session's own current workspace, for a request that has a session
  2. the workspace the user pinned as their default
  3. the workspace they used last
  4. the oldest workspace they are a member of

Each rung is a tiebreak on the one above, so a user with a single membership ties on all four and lands where they always did. That is what makes the switcher a no-op for every instance that exists today.

Membership is the WHERE clause, not the ordering, so a preference pointing at a workspace the user has been removed from — or one that has been deleted — cannot win. It simply stops matching and the next rung answers.

session_id is optional. NULL leaves the LEFT JOIN unmatched and rung 1 dead, which is right for a login (the session does not exist yet), the CLI, and an API key. The join also requires the session to belong to this user, so a borrowed id resolves nothing.

organization_id is optional too, and it is a *bound* rather than a rung: it narrows which workspaces are candidates without touching the precedence. Only one caller passes it, and the reason is M44. An organization-wide API key is a row with a NULL workspace_id, which means "every workspace in **the organization** the key belongs to" — and without this clause the precedence would happily rank a workspace in some *other* organization the owner also belongs to, because membership is the only filter and a person's pinned default is a property of the person rather than of the tenancy. The key would then act in a tenant it was never issued for. Every other caller passes NULL and resolves exactly as it always did.

The organization's own deleted_at is checked as well as the workspace's, and the two are not the same check. ListWorkspacesForUser has always filtered both; this statement filtered only the workspace, so a workspace under a soft-deleted organization could be resolved *into* and never *listed* — the switcher would mark nothing selected, and a browser would show the first entry while the session acted somewhere else. Latent rather than live, and deliberately fixed anyway: DeleteOrganization is a hard DELETE today and nothing in the tree sets organizations.deleted_at, so the two queries agree in practice and would stop agreeing the moment anything soft-deletes an organization. The asymmetry is invisible from either statement alone, which is the reason it survived to be found by review rather than by a user (F25).

func (q *Queries) RestoreLink(ctx context.Context, arg RestoreLinkParams) (Link, error)

func (*Queries) RevokeAPIKey

func (q *Queries) RevokeAPIKey(ctx context.Context, arg RevokeAPIKeyParams) (int64, error)

Idempotent: revoking an already-revoked key keeps the original timestamp and still reports one row, so a repeated call is a success rather than a 404 while a genuinely unknown id is still distinguishable.

func (*Queries) RevokeAPIKeyInOrganization added in v0.2.0

The administrator's revoke, keyed on the organization instead of on the owner.

RevokeAPIKey above is a person disabling their own credential and is the normal path. This one exists because there was otherwise no path at all: a key belonging to somebody else could be *seen* — the rotation records are organization-scoped — and not stopped, so an administrator holding an incident had to wait for its owner. Scoped by organization rather than by workspace because a key is issued into an organization and its id is what an audit record hands the reader.

Returns the owner and the prefix rather than a row count, because this write is audited and the record has to name whose credential was stopped. No row means an id from another organization or none at all, and both answer the same way at the call site.

**Pinned keys only** since M54, and by the predicate rather than by a new clause: `organization_id = $2` is never true of a NULL. That is the right refusal rather than a gap. This statement destroys a credential outright, and for a pinned key that is proportionate — the organization is the key's entire reach, so cutting the reach and cutting the key are the same act. An account-wide key belongs to an account that acts in tenants this administrator has no authority over, and RevokeAPIKeyReachInOrganization below is what they get instead.

func (*Queries) RevokeAPIKeyReachInOrganization added in v0.3.0

func (q *Queries) RevokeAPIKeyReachInOrganization(ctx context.Context, arg RevokeAPIKeyReachInOrganizationParams) (int64, error)

An administrator cutting their organization out of an account-wide key.

Idempotent for the reason RevokeAPIKey is: the second call reports a row and keeps the original timestamp, so repeating it is a success rather than a 404, while an id nobody may act on is still distinguishable.

ON CONFLICT rather than a prior existence check, because two administrators reacting to the same incident is the normal case and neither should see an error about the other.

func (*Queries) RevokeAllUserSessions

func (q *Queries) RevokeAllUserSessions(ctx context.Context, arg RevokeAllUserSessionsParams) error

Used on password change. Anyone who had the old password must be logged out, which is the entire point of changing it. keep_session is optional: pass NULL to revoke everything, or the current session's id to leave the browser the user is changing their password in still signed in.

func (*Queries) RevokeInstancePermission added in v0.2.0

func (q *Queries) RevokeInstancePermission(ctx context.Context, arg RevokeInstancePermissionParams) (int64, error)

Withdraw one instance-level permission from one account.

Returns the row count so the caller can tell "withdrawn" from "they never held it" without a read first. Which permissions may travel this path at all is decided in Go, not here: instance.admin is deliberately not one of them.

func (*Queries) RevokeInvitation added in v0.2.0

func (q *Queries) RevokeInvitation(ctx context.Context, arg RevokeInvitationParams) (int64, error)

Scoped by organization as well as id, so an id from another organization is indistinguishable from one that does not exist: both change zero rows.

Already-revoked and already-redeemed are excluded rather than tolerated. A redeemed invite has produced a member, and reporting "revoked" for it would claim something the tree does not support.

func (*Queries) RevokeLapsedAPIKeyGraces added in v0.2.0

func (q *Queries) RevokeLapsedAPIKeyGraces(ctx context.Context) (int64, error)

Auto-revocation, from housekeeping.

revoked_at is set to the moment the window closed rather than to now(), so the list says when the key stopped working instead of when the job noticed. Nothing depends on this running: authentication already refuses a key past grace_expires_at. What this buys is a key list that agrees with the behaviour.

func (*Queries) RevokeLapsedInvitation added in v0.2.0

func (q *Queries) RevokeLapsedInvitation(ctx context.Context, arg RevokeLapsedInvitationParams) (int64, error)

Clears an expired invite out of the outstanding slot so a replacement can be issued.

The partial unique index cannot exclude expired rows — `now()` is not immutable, so Postgres will not index on it — which means an invite that lapsed still occupies the address. This runs immediately before the insert, in the same transaction, and touches nothing that is still redeemable.

func (*Queries) RevokeSession

func (q *Queries) RevokeSession(ctx context.Context, id uuid.UUID) error

func (*Queries) RollupDestinationDaily added in v0.2.0

func (q *Queries) RollupDestinationDaily(ctx context.Context, arg RollupDestinationDailyParams) error

The per-destination breakdown a split test is read from (M36).

A pass of its own rather than a seventh row in RollupDimensionDaily's LATERAL VALUES, and the reason is cost rather than tidiness. That expansion runs for every click on the instance; adding a row to it would grow the sort and the upsert count by a sixth, permanently, for a column that is NULL on every link that runs no split test. Here the `destination_id IS NOT NULL` filter is served by the partial index migration 02200 creates, so on an instance with no split tests this reads an empty index and writes nothing.

The value is the destination id as text, into the same `link_dimension_daily` table under the dimension name `destination`, so the breakdown is capped, rolled up and read by exactly the query every other breakdown is read by. The reader resolves ids to URLs; storing the URL here instead would freeze it at the moment of the rollup and make an edited destination look like two.

Bots excluded, like every other dimension: a split test scored on crawler traffic is a split test with a wrong answer.

func (*Queries) RollupDimensionDaily

func (q *Queries) RollupDimensionDaily(ctx context.Context, arg RollupDimensionDailyParams) error

Every dimension in one pass over click_events.

This was six UNION ALL branches, one per dimension, reading the same rows six times. Measured on the load-test dataset (5.7M events, ~830k inside the recomputed window), that shape sorted 6.2M rows through an external merge that spilled 471 MB of temp files, every 60 seconds. Reading once and expanding each row with LATERAL VALUES lets the sort use the index's link_id ordering, so it runs incrementally in memory instead — peak 152 kB per group, no temp files.

Wall clock is unchanged (~20s either way), and that is the finding rather than a disappointment: the time is in the 553k upserts a whole-day recompute implies, not in reading the events. See docs/slo.md. This version is kept because eliminating half a gigabyte of temp I/O per run is worth having on any host smaller than the one it was measured on; it is not a fix for the job's cost.

The output is identical: same grouping keys, same aggregates, same conflict resolution. TestDimensionRollupMatchesAPerDimensionAggregate checks that against a per-dimension aggregate written the other way round.

func (*Queries) RollupLinkDaily

func (q *Queries) RollupLinkDaily(ctx context.Context, arg RollupLinkDailyParams) error

Recompute per-link daily totals for a window.

Idempotent by construction: it recomputes a whole day from the raw events and upserts, so running it twice, or after a crash mid-run, converges to the same numbers. An incremental "add what is new" design would double-count on any retry.

func (*Queries) RollupWorkspaceDaily

func (q *Queries) RollupWorkspaceDaily(ctx context.Context, arg RollupWorkspaceDailyParams) error

func (*Queries) RotateWebhookSecret added in v0.2.0

func (q *Queries) RotateWebhookSecret(ctx context.Context, arg RotateWebhookSecretParams) error

func (*Queries) SetBotBlockingForEveryDomain added in v0.2.0

func (q *Queries) SetBotBlockingForEveryDomain(ctx context.Context, arg SetBotBlockingForEveryDomainParams) ([]SetBotBlockingForEveryDomainRow, error)

Both switches at once, because they are one setting with two halves and the CHECK in 01800 refuses the combination that writing them separately would pass through on the way. That applies row by row, so a propagation that touched one column would be refused by the constraint on the way out.

**Every undeleted domain, not only the default (F89).** This was `WHERE is_default` until M45, which was the whole truth while the instance default was the only domain there was. M40 added verified custom hostnames and `ResolveAliasForRedirect` reads the policy from the link's *own* domain row, so an operator who turned blocking on — even enforced — got no blocking on any link served on a custom hostname, and any workspace could open that hole for itself by registering one. Plan.md's "the domain's setting is instance-wide" is the claim being restored, and this is what makes it true.

**Every updated row comes back, not just the default**, because each one is a cache invalidation the caller owes: a snapshot carries its domain's policy so the redirect path needs no second lookup, so every cached alias under every domain touched here is now wrong. The default is marked rather than sorted for, since it is the row the settings surfaces read and the hostname they name.

func (*Queries) SetDefaultDomainRootRedirect

func (q *Queries) SetDefaultDomainRootRedirect(ctx context.Context, rootRedirectUrl *string) (SetDefaultDomainRootRedirectRow, error)

NULL clears it, which restores the 404 the root answered before anyone set anything.

func (*Queries) SetDefaultWorkspaceForUser added in v0.2.0

func (q *Queries) SetDefaultWorkspaceForUser(ctx context.Context, arg SetDefaultWorkspaceForUserParams) (int64, error)

Pins a workspace as where new sessions start, or clears the pin.

NULL is a real value here and means "follow last-used", which is the default the control offers. Clearing therefore needs no membership check; setting needs the same one as above.

func (*Queries) SetDomainRootRedirect added in v0.2.0

func (q *Queries) SetDomainRootRedirect(ctx context.Context, arg SetDomainRootRedirectParams) (Domain, error)

Where a verified hostname's own root sends a visitor (M40).

The same column 00800 added for the instance default, addressed by id instead of by `is_default`. A custom hostname is a bare domain somebody will type, and answering 404 there is a choice its owner should get to make rather than inherit from the instance.

NULL clears it, restoring the 404.

func (*Queries) SetJobWatermark

func (q *Queries) SetJobWatermark(ctx context.Context, arg SetJobWatermarkParams) error

Runs only after the rollup returned without error, which is what makes last_success_at mean what its name says. last_run_at cannot: RecordJobFailure stamps it too, so a job failing on every tick would report itself fresh forever and the staleness alert would never fire.

func (*Queries) SetLastWorkspaceForUser added in v0.2.0

func (q *Queries) SetLastWorkspaceForUser(ctx context.Context, arg SetLastWorkspaceForUserParams) (int64, error)

Remembers a selection, and refuses one the user is not entitled to.

The membership check is in the statement rather than in a preceding SELECT so there is no window between the two. Zero rows means "not yours or not there", which the caller reports as not-found: a workspace id must not be probeable for existence.

func (*Queries) SetPrimaryDestination

func (q *Queries) SetPrimaryDestination(ctx context.Context, arg SetPrimaryDestinationParams) error
func (q *Queries) SetQRCodeLogo(ctx context.Context, arg SetQRCodeLogoParams) (int64, error)

--- QR logos (M50.5) -------------------------------------------------------- Stores the re-encoded image against one code.

**One statement, so setting and replacing are the same operation and neither has a gap in it.** A logo that replaced another leaves nothing behind to collect: the previous value is overwritten by this write rather than deleted by a second one, which is the property the storage decision was chosen for (D134) and the reason "replacing removes the artefact it replaced" needs no code of its own.

The bytes are already bounded — the request body by http.MaxBytesReader, the decode by qr.MaxDecodedLogoPixels, and this value by qr.MaxLogoStoredBytes, which internal/qr enforces rather than assumes. Since D180, qr.MaxLogoPixels bounds the *stored* artefact alone and refuses nothing: an image above it is resampled down to it before it reaches this statement.

func (*Queries) SetSessionWorkspace added in v0.2.0

func (q *Queries) SetSessionWorkspace(ctx context.Context, arg SetSessionWorkspaceParams) (int64, error)

Moves one session, and only the session that asked.

Scoped by user_id as well as id so a session id from elsewhere cannot be repointed, and revoked sessions are excluded because moving one would be writing to a credential that no longer authenticates.

func (*Queries) SetUpdateCheckEnabled added in v0.3.0

func (q *Queries) SetUpdateCheckEnabled(ctx context.Context, enabled bool) (int64, error)

Instance-level settings: the answers an operator gives about the box rather than about a tenant in it (M55). One row, guaranteed by 04300's primary key, so every statement here is written against `id` and returns or touches exactly one.

**The column is nullable and NULL means unanswered** (D164). Three statements rather than two because that third state has to be readable — the prompt an upgraded instance gets at its first administrative sign-in is drawn from exactly one fact, *has anybody answered this yet*, and there is nowhere else to read it from. A `GetInstanceSettings` returning the whole row was written, generated unused into the Querier interface, and removed: nothing renders these settings, and a read of everything is the shape a settings API grows out of before anything has asked for one.

Two statements write the answer, and the difference between them is which state they are allowed to leave. Setup may rewrite, because nothing has been committed to until the instance is claimed; the principal answers once. Record the operator's answer to the first-run prompt, at setup (D149).

**Unconditional, which is deliberate and is the difference from AnswerUpdateCheck below.** The instance is unclaimed — `SetUpdateCheckAtSetup` has just counted the users to be sure of it — so an answer already sitting in the row is a previous setup attempt whose `Register` failed, and the operator retrying with the box unticked must be able to replace a yes with a no. A conditional write here would make the first attempt's answer the permanent one.

The row count is the check: the row is inserted by migration 04300, so zero means the settings row is missing and the answer went nowhere. The setup path reads it rather than assuming, because the failure it guards against is an operator declining the update check and being checked on anyway.

func (*Queries) SoftDeleteDomain added in v0.2.0

func (q *Queries) SoftDeleteDomain(ctx context.Context, id uuid.UUID) (int64, error)

Soft, unlike a folder. A domain is the namespace its links' aliases live in and `links.domain_id` is NOT NULL with no cascade, so a hard delete is refused by the database the moment one link exists; a soft delete keeps the row that every historic click event and reserved alias still points at.

func (q *Queries) SoftDeleteLink(ctx context.Context, arg SoftDeleteLinkParams) (SoftDeleteLinkRow, error)

Soft delete with a purge deadline rather than an immediate DELETE. Restoring a link someone deleted by accident is a common request, and the alias stays reserved while the row exists.

func (*Queries) SoftDeleteUser added in v0.3.0

func (q *Queries) SoftDeleteUser(ctx context.Context, userID uuid.UUID) (int64, error)

The deletion itself: the first writer `status` and `deleted_at` have ever had.

`status = 'deleted'` and `deleted_at` are set together and only together. The timestamp is what every query in this product filters on and what releases the address through the partial unique index; the status is what a person reading the row sees. Setting one without the other would make the two disagree about the same fact, which is the state the CHECK constraint cannot catch.

`password_hash` is **not** cleared here. Scrubbing is the erasure pass's, and clearing it early would take the one field that makes a mistaken deletion recoverable by an operator inside the sweep's window, while ending no access that the session and key rows going in the same transaction have not already ended.

func (*Queries) SpendMFARecoveryCode added in v0.3.0

func (q *Queries) SpendMFARecoveryCode(ctx context.Context, arg SpendMFARecoveryCodeParams) (int64, error)

Match a presented code and spend it, in one statement.

Scoped by `user_id` as well as by the hash. The pending login already names the account, and matching on the hash alone would make this table a global lookup — correct today, because the hash index is unique, and one refactor away from a code minted for one account opening another.

`used_at IS NULL` is in the predicate for the reason the replay guard's comparison is: single use has to be decided by the statement that spends it, or two simultaneous presentations of the same code both pass their check.

func (*Queries) TouchAPIKeys

func (q *Queries) TouchAPIKeys(ctx context.Context, arg TouchAPIKeysParams) error

Batch write of last_used_at, from the coalescing tracker rather than from the request path: authenticating a key must not cost a synchronous write.

GREATEST guards against a late batch moving the timestamp backwards, which two processes flushing out of order would otherwise do.

func (*Queries) TouchSession

func (q *Queries) TouchSession(ctx context.Context, id uuid.UUID) error

Idle expiry is measured from last_seen_at. Updated at most once a minute by the caller, because writing on every request would turn a read-mostly path into a write on the hottest authenticated query.

func (q *Queries) UnassignCampaignLinks(ctx context.Context, arg UnassignCampaignLinksParams) (int64, error)

Takes every link out of a deleted campaign.

Run in the same transaction as DeleteCampaign. Without it the links keep an id pointing at a row no query returns, which is a link filtered by a campaign that is not in the campaign list — the invisible-rows failure 02400 describes for folders, in the one place the schema does not prevent it.

func (*Queries) UnverifyDomain added in v0.2.0

func (q *Queries) UnverifyDomain(ctx context.Context, arg UnverifyDomainParams) (Domain, error)

The end of the grace window, and the only place serving stops on its own.

`verified_at` is cleared, so the next ListVerifiedDomains does not return the row, so every replica's host cache drops it and the hostname goes back to ops-only 404. ssl_status goes with it: this instance will stop answering Caddy's ask for the name, which is the other half of no longer serving it.

The failing streak is *kept*. The page has to be able to say "this stopped being served at 03:00 because it had been failing since yesterday", and clearing the anchor here would throw away the only record of why.

func (*Queries) UpdateAutomationRule added in v0.2.0

func (q *Queries) UpdateAutomationRule(ctx context.Context, arg UpdateAutomationRuleParams) (AutomationRule, error)

Partial: a NULL parameter leaves its column alone, the shape every other update in this schema has.

**Re-arming is in this statement and not a second one.** A rule switched from disabled to enabled has its watermark moved to the arming instant, so a rule that was off for a month does not fire for a month of backlog the moment somebody flips the switch. Switching one *off* leaves the watermark where it is, because a disabled rule is not evaluated at all and the value is what a reader is shown.

The re-arm resets **both halves** of the watermark. The pair describes one position in the match order, and a stale subject id beside a fresh instant would describe a position that never existed — one that admits subjects tied on the arming instant whose ids happen to sort above the old boundary. NULL is the "instant fully spent" spelling, the same one creation uses.

func (*Queries) UpdateCampaign added in v0.2.0

func (q *Queries) UpdateCampaign(ctx context.Context, arg UpdateCampaignParams) (Campaign, error)

Partial update through COALESCE, like UpdateLink. The two schedule bounds are three-valued through their own clear flags, because "leave the end date alone" and "this campaign no longer ends" are different requests and one nullable parameter cannot express both.

func (*Queries) UpdateCheckAnswered added in v0.3.0

func (q *Queries) UpdateCheckAnswered(ctx context.Context) (bool, error)

Has anybody answered the update-check question on this instance?

The whole of what the prompt needs, and deliberately not the value: whether the check is *on* is decided inside ClaimUpdateCheck, and a second reader of that fact is a second place for it to be got wrong. This answers only *is the question still open*, which is what decides whether an administrator is asked.

func (*Queries) UpdateDestinationURL

func (q *Queries) UpdateDestinationURL(ctx context.Context, arg UpdateDestinationURLParams) error

The trigger on destinations mirrors this into links.primary_url, so the hot path never joins.

Narrowed to the *primary* destination by M34, and the narrowing is the point rather than tidying. Until routing rules existed a link had exactly one destination row, so matching on link_id alone matched it; a rule target is a second row on the same link, and this query would have rewritten every one of them to the link's own URL the next time somebody edited the link. Every rule on the link would silently start pointing at the same place, which is indistinguishable from the rules having stopped working.

Matched through links.primary_destination_id rather than through `position = 0`, because that column is what the sync trigger keys on and what the rest of the schema treats as the authority. Two definitions of "the primary" is how they come to disagree.

func (q *Queries) UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error)

COALESCE with sqlc.narg gives partial update: a NULL argument leaves the column alone, so PATCH semantics need no dynamic SQL.

func (*Queries) UpdateMembershipRole added in v0.2.0

func (q *Queries) UpdateMembershipRole(ctx context.Context, arg UpdateMembershipRoleParams) (int64, error)

Scoped by organization as well as id, so the authorization decision the service made cannot be applied to a row in another tenant.

func (*Queries) UpdateQRCodeLabel added in v0.3.0

func (q *Queries) UpdateQRCodeLabel(ctx context.Context, arg UpdateQRCodeLabelParams) (int64, error)

Renames one code. The slug is untouched on purpose: it is printed, and a rename that moved it would break every copy already in the world.

func (*Queries) UpdateRoutingRule added in v0.2.0

func (q *Queries) UpdateRoutingRule(ctx context.Context, arg UpdateRoutingRuleParams) (RoutingRule, error)

Partial update, same COALESCE-with-narg shape as UpdateLink. The destination is not here: changing where a rule points is a write to its `destinations` row, so that the URL, its host and the tier check that accepted it stay in one place.

func (*Queries) UpdateRuleDestinationURL added in v0.2.0

func (q *Queries) UpdateRuleDestinationURL(ctx context.Context, arg UpdateRuleDestinationURLParams) error

Scoped to one destination id, unlike UpdateDestinationURL. A rule target and the link's own destination are two rows on the same link now, and the wrong one of these two queries would move both.

func (*Queries) UpdateUserPassword

func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error

func (*Queries) UpdateVariantDestination added in v0.2.0

func (q *Queries) UpdateVariantDestination(ctx context.Context, arg UpdateVariantDestinationParams) error

The URL, the weight, or both. Scoped to one destination id for the reason UpdateRuleDestinationURL is: a link's own destination and its arms are rows in the same table on the same link.

func (*Queries) UpdateVariantRule added in v0.2.0

func (q *Queries) UpdateVariantRule(ctx context.Context, arg UpdateVariantRuleParams) (RoutingRule, error)

Only the enabled flag, because that is the only thing on the rule row a variant has. A weight is a write to the destination; a kind is not editable at all, since changing one arm's kind would mix two kinds on a link and the service refuses that outright.

func (*Queries) UpdateWebhook added in v0.2.0

func (q *Queries) UpdateWebhook(ctx context.Context, arg UpdateWebhookParams) (UpdateWebhookRow, error)

Partial: a NULL parameter leaves its column alone. Same shape as every other update in this schema, so "absent" and "empty" stay different — an empty events array is a real request to subscribe to nothing.

func (*Queries) UpsertEnvBlockedDestination added in v0.2.0

func (q *Queries) UpsertEnvBlockedDestination(ctx context.Context, arg UpsertEnvBlockedDestinationParams) error

Writes one entry from LINKCTRL_DESTINATION_BLOCKLIST at boot.

ON CONFLICT DO UPDATE rather than DO NOTHING: an operator who moves a host into their environment expects the environment to own it from then on, and a row left claiming it came from a review would send M31 looking for a review that never happened. created_at is left alone, because the entry is the same entry it was before the restart.

func (*Queries) UpsertQRCode added in v0.2.0

func (q *Queries) UpsertQRCode(ctx context.Context, arg UpsertQRCodeParams) (UpsertQRCodeRow, error)

One row per (link, slug), which qr_codes_link_slug_key (03700) is what makes true. Without the unique index this is two concurrent inserts and a link with two codes answering to one name.

The label is not in the DO UPDATE list. This statement is how a *style* is written, and a style write must not silently rename the code it is drawn for; UpdateQRCodeLabel is the operation that renames one. **Nor is the logo**, for the same reason and with more at stake: restyling a code must not throw away the image somebody uploaded to it, and the insert branch leaves the column at its NULL default because a code that has just come into being has no logo. `is_default` is not in the DO UPDATE list either, and for the strongest of the three reasons: which code an untagged scan resolves through is not something a style write may move. ClearDefaultQRCode and MarkDefaultQRCode are the pair that moves it, and they are the only pair that does.

func (*Queries) WithTx

func (q *Queries) WithTx(tx pgx.Tx) *Queries

type RecordFailedLoginParams

type RecordFailedLoginParams struct {
	ID             uuid.UUID
	Threshold      int32
	LockoutSeconds int32
}

type RecordFailedLoginRow

type RecordFailedLoginRow struct {
	FailedLoginCount int32
	LockedUntil      *time.Time
}

type RecordJobFailureParams

type RecordJobFailureParams struct {
	Job       string
	LastError *string
}

type RenameDomainParams added in v0.2.0

type RenameDomainParams struct {
	ID                uuid.UUID
	Hostname          string
	VerificationToken *string
}

type RenameFolderParams added in v0.2.0

type RenameFolderParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
}

type RenameWorkspaceParams added in v0.2.0

type RenameWorkspaceParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
	Name           string
	Slug           string
}

type ReserveAliasParams

type ReserveAliasParams struct {
	DomainID uuid.UUID
	Alias    string
	Reason   string
}

type ReservedAlias

type ReservedAlias struct {
	DomainID   uuid.UUID
	Alias      string
	Reason     string
	ReservedAt time.Time
}

type ResolveAliasForRedirectParams

type ResolveAliasForRedirectParams struct {
	DomainID uuid.UUID
	Alias    string
}

type ResolveAliasForRedirectRow

type ResolveAliasForRedirectRow struct {
	ID                uuid.UUID
	WorkspaceID       uuid.UUID
	DomainID          uuid.UUID
	Alias             string
	PrimaryUrl        string
	Status            string
	ExpiresAt         *time.Time
	PasswordHash      *string
	MaxClicks         *int64
	OneTime           bool
	RequireSignature  bool
	ForwardQuery      bool
	ForwardPath       bool
	BotBlocking       string
	BlockBots         bool
	BlockBotsEnforced bool
	Rules             []byte
	CodeSlugs         []byte
}

type ResolveDefaultDomainRow

type ResolveDefaultDomainRow struct {
	ID       uuid.UUID
	Hostname string
}

type ResolveDomainByHostnameRow

type ResolveDomainByHostnameRow struct {
	ID             uuid.UUID
	OrganizationID *uuid.UUID
	Hostname       string
}

type ResolveOrganizationForAPIKeyParams added in v0.3.0

type ResolveOrganizationForAPIKeyParams struct {
	UserID   uuid.UUID
	ApiKeyID uuid.UUID
}

type ResolveOrganizationForAPIKeyRow added in v0.3.0

type ResolveOrganizationForAPIKeyRow struct {
	ID                    uuid.UUID
	BarredOrganizationIds []uuid.UUID
}

type ResolveWorkspaceForUserParams added in v0.2.0

type ResolveWorkspaceForUserParams struct {
	SessionID      *uuid.UUID
	UserID         uuid.UUID
	OrganizationID *uuid.UUID
}

type RestoreLinkParams

type RestoreLinkParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type RevokeAPIKeyInOrganizationParams added in v0.2.0

type RevokeAPIKeyInOrganizationParams struct {
	ID             uuid.UUID
	OrganizationID *uuid.UUID
}

type RevokeAPIKeyInOrganizationRow added in v0.2.0

type RevokeAPIKeyInOrganizationRow struct {
	UserID uuid.UUID
	Prefix string
}

type RevokeAPIKeyParams

type RevokeAPIKeyParams struct {
	ID     uuid.UUID
	UserID uuid.UUID
}

type RevokeAPIKeyReachInOrganizationParams added in v0.3.0

type RevokeAPIKeyReachInOrganizationParams struct {
	ApiKeyID       uuid.UUID
	OrganizationID uuid.UUID
	RevokedBy      *uuid.UUID
}

type RevokeAllUserSessionsParams

type RevokeAllUserSessionsParams struct {
	UserID      uuid.UUID
	KeepSession *uuid.UUID
}

type RevokeInstancePermissionParams added in v0.2.0

type RevokeInstancePermissionParams struct {
	UserID     uuid.UUID
	Permission string
}

type RevokeInvitationParams added in v0.2.0

type RevokeInvitationParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
}

type RevokeLapsedInvitationParams added in v0.2.0

type RevokeLapsedInvitationParams struct {
	OrganizationID uuid.UUID
	Email          string
}

type Role

type Role struct {
	ID             uuid.UUID
	OrganizationID *uuid.UUID
	Slug           string
	Name           string
	Description    string
	IsBuiltin      bool
	Rank           int32
	CreatedAt      time.Time
}

type RolePermission

type RolePermission struct {
	RoleID       uuid.UUID
	PermissionID uuid.UUID
}

type RollupDestinationDailyParams added in v0.2.0

type RollupDestinationDailyParams struct {
	WindowStart time.Time
	WindowEnd   time.Time
}

type RollupDimensionDailyParams

type RollupDimensionDailyParams struct {
	WindowStart time.Time
	WindowEnd   time.Time
}

type RollupLinkDailyParams

type RollupLinkDailyParams struct {
	WindowStart time.Time
	WindowEnd   time.Time
}

type RollupWorkspaceDailyParams

type RollupWorkspaceDailyParams struct {
	WindowStart time.Time
	WindowEnd   time.Time
}

type RotateWebhookSecretParams added in v0.2.0

type RotateWebhookSecretParams struct {
	Secret      []byte
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type RoutingRule

type RoutingRule struct {
	ID            uuid.UUID
	LinkID        uuid.UUID
	WorkspaceID   uuid.UUID
	DestinationID *uuid.UUID
	Priority      int32
	Conditions    []byte
	Kind          string
	Enabled       bool
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

type Session

type Session struct {
	ID          uuid.UUID
	UserID      uuid.UUID
	TokenHash   []byte
	IpPrefix    *string
	UserAgent   *string
	CreatedAt   time.Time
	LastSeenAt  time.Time
	ExpiresAt   time.Time
	RevokedAt   *time.Time
	WorkspaceID *uuid.UUID
}

type SetBotBlockingForEveryDomainParams added in v0.2.0

type SetBotBlockingForEveryDomainParams struct {
	BlockBots         bool
	BlockBotsEnforced bool
}

type SetBotBlockingForEveryDomainRow added in v0.2.0

type SetBotBlockingForEveryDomainRow struct {
	ID                uuid.UUID
	Hostname          string
	RootRedirectUrl   *string
	BlockBots         bool
	BlockBotsEnforced bool
	IsDefault         bool
}

type SetDefaultDomainRootRedirectRow

type SetDefaultDomainRootRedirectRow struct {
	ID                uuid.UUID
	Hostname          string
	RootRedirectUrl   *string
	BlockBots         bool
	BlockBotsEnforced bool
}

type SetDefaultWorkspaceForUserParams added in v0.2.0

type SetDefaultWorkspaceForUserParams struct {
	WorkspaceID *uuid.UUID
	UserID      uuid.UUID
}

type SetDomainRootRedirectParams added in v0.2.0

type SetDomainRootRedirectParams struct {
	RootRedirectUrl *string
	ID              uuid.UUID
}

type SetJobWatermarkParams

type SetJobWatermarkParams struct {
	Job       string
	Watermark *time.Time
}

type SetLastWorkspaceForUserParams added in v0.2.0

type SetLastWorkspaceForUserParams struct {
	WorkspaceID uuid.UUID
	UserID      uuid.UUID
}

type SetPrimaryDestinationParams

type SetPrimaryDestinationParams struct {
	ID                   uuid.UUID
	PrimaryDestinationID *uuid.UUID
}

type SetQRCodeLogoParams added in v0.3.0

type SetQRCodeLogoParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type SetSessionWorkspaceParams added in v0.2.0

type SetSessionWorkspaceParams struct {
	WorkspaceID uuid.UUID
	SessionID   uuid.UUID
	UserID      uuid.UUID
}

type SoftDeleteLinkParams

type SoftDeleteLinkParams struct {
	RetentionDays int32
	ID            uuid.UUID
	WorkspaceID   uuid.UUID
}

type SoftDeleteLinkRow

type SoftDeleteLinkRow struct {
	ID         uuid.UUID
	Alias      string
	DomainID   uuid.UUID
	ClickCount int64
}

type SpendMFARecoveryCodeParams added in v0.3.0

type SpendMFARecoveryCodeParams struct {
	UserID   uuid.UUID
	CodeHash []byte
}

type Tag

type Tag struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Name        string
	Color       string
	CreatedAt   time.Time
}

type TouchAPIKeysParams

type TouchAPIKeysParams struct {
	Ids    []uuid.UUID
	UsedAt []time.Time
}

type UnassignCampaignLinksParams added in v0.2.0

type UnassignCampaignLinksParams struct {
	CampaignID  *uuid.UUID
	WorkspaceID uuid.UUID
}

type UnverifyDomainParams added in v0.2.0

type UnverifyDomainParams struct {
	ID                uuid.UUID
	VerificationError string
}

type UpdateAutomationRuleParams added in v0.2.0

type UpdateAutomationRuleParams struct {
	Name          *string
	Trigger       *string
	TriggerConfig []byte
	Actions       []byte
	Enabled       *bool
	RearmedAt     *time.Time
	ID            uuid.UUID
	WorkspaceID   uuid.UUID
}

type UpdateCampaignParams added in v0.2.0

type UpdateCampaignParams struct {
	Name          *string
	Slug          *string
	Description   *string
	ClearStartsAt bool
	StartsAt      *time.Time
	ClearEndsAt   bool
	EndsAt        *time.Time
	ID            uuid.UUID
	WorkspaceID   uuid.UUID
}

type UpdateDestinationURLParams

type UpdateDestinationURLParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	UrlHost     string
}

type UpdateLinkParams

type UpdateLinkParams struct {
	Title            *string
	Description      *string
	ClearExpiry      bool
	ExpiresAt        *time.Time
	Alias            *string
	ForwardQuery     *bool
	ForwardPath      *bool
	BotBlocking      *string
	ClearPassword    bool
	PasswordHash     *string
	ClearMaxClicks   bool
	MaxClicks        *int64
	OneTime          *bool
	RequireSignature *bool
	ClearFolder      bool
	FolderID         *uuid.UUID
	ClearCampaign    bool
	CampaignID       *uuid.UUID
	ID               uuid.UUID
	WorkspaceID      uuid.UUID
}

type UpdateMembershipRoleParams added in v0.2.0

type UpdateMembershipRoleParams struct {
	ID             uuid.UUID
	OrganizationID uuid.UUID
	RoleID         uuid.UUID
}

type UpdateQRCodeLabelParams added in v0.3.0

type UpdateQRCodeLabelParams struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Label       string
}

type UpdateRoutingRuleParams added in v0.2.0

type UpdateRoutingRuleParams struct {
	Priority    *int32
	Conditions  []byte
	Enabled     *bool
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type UpdateRuleDestinationURLParams added in v0.2.0

type UpdateRuleDestinationURLParams struct {
	ID          uuid.UUID
	Url         string
	UrlHost     string
	WorkspaceID uuid.UUID
}

type UpdateUserPasswordParams

type UpdateUserPasswordParams struct {
	ID           uuid.UUID
	PasswordHash *string
}

type UpdateVariantDestinationParams added in v0.2.0

type UpdateVariantDestinationParams struct {
	Url         *string
	UrlHost     *string
	Weight      *int32
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type UpdateVariantRuleParams added in v0.2.0

type UpdateVariantRuleParams struct {
	Enabled     *bool
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type UpdateWebhookParams added in v0.2.0

type UpdateWebhookParams struct {
	Url         *string
	Events      []string
	Description *string
	Enabled     *bool
	ID          uuid.UUID
	WorkspaceID uuid.UUID
}

type UpdateWebhookRow added in v0.2.0

type UpdateWebhookRow struct {
	ID          uuid.UUID
	WorkspaceID uuid.UUID
	Url         string
	Events      []string
	Description string
	Enabled     bool
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type UpsertEnvBlockedDestinationParams added in v0.2.0

type UpsertEnvBlockedDestinationParams struct {
	Host   string
	Reason string
}

type UpsertQRCodeParams added in v0.2.0

type UpsertQRCodeParams struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Slug        string
	Label       string
	Style       []byte
	IsDefault   bool
}

type UpsertQRCodeRow added in v0.3.0

type UpsertQRCodeRow struct {
	ID          uuid.UUID
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	Style       []byte
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Label       string
	Slug        string
	IsDefault   bool
}

type User

type User struct {
	ID                 uuid.UUID
	Email              string
	EmailLower         *string
	EmailVerifiedAt    *time.Time
	Name               string
	PasswordHash       *string
	Status             string
	FailedLoginCount   int32
	LockedUntil        *time.Time
	MfaSecret          *string
	MfaEnabledAt       *time.Time
	AnonymizedAt       *time.Time
	LastLoginAt        *time.Time
	CreatedAt          time.Time
	UpdatedAt          time.Time
	DeletedAt          *time.Time
	DefaultWorkspaceID *uuid.UUID
	LastWorkspaceID    *uuid.UUID
	MfaLastStep        *int64
}

type Visitor

type Visitor struct {
	VisitorHash []byte
	LinkID      uuid.UUID
	WorkspaceID uuid.UUID
	SeenOn      time.Time
	FirstSeenAt time.Time
	OccurredAt  time.Time
}

type Webhook

type Webhook struct {
	ID           uuid.UUID
	WorkspaceID  uuid.UUID
	Url          string
	Secret       []byte
	Subscription []byte
	Enabled      bool
	CreatedAt    time.Time
	UpdatedAt    time.Time
	Events       []string
	Description  string
}

type WebhookDelivery

type WebhookDelivery struct {
	ID            uuid.UUID
	WebhookID     uuid.UUID
	Event         string
	Payload       []byte
	Status        string
	Attempts      int32
	ResponseCode  *int32
	NextAttemptAt *time.Time
	CreatedAt     time.Time
	CompletedAt   *time.Time
	LastError     string
}

type Workspace

type Workspace struct {
	ID                     uuid.UUID
	OrganizationID         uuid.UUID
	Name                   string
	Slug                   string
	AnalyticsRetentionDays *int32
	CreatedAt              time.Time
	UpdatedAt              time.Time
	DeletedAt              *time.Time
	SigningSecret          []byte
}

type WorkspaceClickDaily

type WorkspaceClickDaily struct {
	WorkspaceID    uuid.UUID
	Day            time.Time
	Clicks         int64
	UniqueVisitors int64
	BotClicks      int64
	ActiveLinks    int64
	FinalizedAt    *time.Time
}

Jump to

Keyboard shortcuts

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