Documentation
¶
Index ¶
- type AnalyticsSalt
- type ApiKey
- type ArchiveLinkParams
- type AttachTagParams
- type AuditLog
- type AutomationRule
- type Campaign
- type ClickEvent
- type CountLinksParams
- type CreateAPIKeyParams
- type CreateDestinationParams
- type CreateLinkParams
- type CreateMembershipParams
- type CreateOrganizationParams
- type CreateSaltParams
- type CreateSessionParams
- type CreateTagParams
- type CreateUserParams
- type CreateWorkspaceParams
- type DBTX
- type DeleteTagParams
- type Destination
- type Domain
- type Folder
- type GetAPIKeyByPrefixRow
- type GetDefaultDomainSettingsRow
- type GetLinkByAliasParams
- type GetLinkDimensionsParams
- type GetLinkDimensionsRow
- type GetLinkParams
- type GetLinkStatsParams
- type GetLinkStatsRow
- type GetLinkTagsRow
- type GetRecentClicksParams
- type GetRecentClicksRow
- type GetSessionByTokenHashRow
- type GetTagByNameParams
- type GetUserPermissionsParams
- type GetUserRoleInWorkspaceParams
- type GetUserRoleInWorkspaceRow
- type GetWorkspaceDefaultDomainRow
- type GetWorkspaceStatsParams
- type GetWorkspaceStatsRow
- type GetWorkspaceTotalsParams
- type GetWorkspaceTotalsRow
- type IsAliasTakenParams
- type JobState
- type Link
- type LinkClickDaily
- type LinkDimensionDaily
- type LinkTag
- type ListAPIKeysForUserParams
- type ListAPIKeysForUserRow
- type ListLinksParams
- type ListLinksRow
- type ListTagsRow
- type ListUserSessionsRow
- type ListUsersRow
- type Membership
- type Notification
- type Organization
- type Permission
- type PurgeExpiredLinksRow
- type QrCode
- type Querier
- type Queries
- func (q *Queries) ArchiveLink(ctx context.Context, arg ArchiveLinkParams) (Link, error)
- func (q *Queries) AttachTag(ctx context.Context, arg AttachTagParams) error
- func (q *Queries) CountClickEvents(ctx context.Context, workspaceID uuid.UUID) (int64, error)
- func (q *Queries) CountLinks(ctx context.Context, arg CountLinksParams) (int64, error)
- func (q *Queries) CountUsers(ctx context.Context) (int64, error)
- func (q *Queries) CreateAPIKey(ctx context.Context, arg CreateAPIKeyParams) (ApiKey, error)
- func (q *Queries) CreateDestination(ctx context.Context, arg CreateDestinationParams) (Destination, error)
- func (q *Queries) CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error)
- func (q *Queries) CreateMembership(ctx context.Context, arg CreateMembershipParams) (Membership, error)
- func (q *Queries) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
- func (q *Queries) CreateSalt(ctx context.Context, arg CreateSaltParams) ([]byte, error)
- func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
- func (q *Queries) CreateTag(ctx context.Context, arg CreateTagParams) (Tag, error)
- func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
- func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, error)
- func (q *Queries) DeleteExpiredSessions(ctx context.Context) (int64, error)
- func (q *Queries) DeleteRevokedAPIKeys(ctx context.Context) (int64, error)
- func (q *Queries) DeleteTag(ctx context.Context, arg DeleteTagParams) (int64, error)
- func (q *Queries) DetachAllTags(ctx context.Context, linkID uuid.UUID) error
- func (q *Queries) GetAPIKeyByPrefix(ctx context.Context, prefix string) (GetAPIKeyByPrefixRow, error)
- func (q *Queries) GetDefaultDomainSettings(ctx context.Context) (GetDefaultDomainSettingsRow, error)
- func (q *Queries) GetDefaultWorkspaceForUser(ctx context.Context, userID uuid.UUID) (Workspace, error)
- func (q *Queries) GetJobWatermark(ctx context.Context, job string) (*time.Time, error)
- func (q *Queries) GetLink(ctx context.Context, arg GetLinkParams) (Link, error)
- func (q *Queries) GetLinkByAlias(ctx context.Context, arg GetLinkByAliasParams) (Link, error)
- func (q *Queries) GetLinkDimensions(ctx context.Context, arg GetLinkDimensionsParams) ([]GetLinkDimensionsRow, error)
- func (q *Queries) GetLinkStats(ctx context.Context, arg GetLinkStatsParams) ([]GetLinkStatsRow, error)
- func (q *Queries) GetLinkTags(ctx context.Context, linkID uuid.UUID) ([]GetLinkTagsRow, error)
- func (q *Queries) GetRecentClicks(ctx context.Context, arg GetRecentClicksParams) ([]GetRecentClicksRow, error)
- func (q *Queries) GetRoleBySlug(ctx context.Context, slug string) (Role, error)
- func (q *Queries) GetSalt(ctx context.Context, validOn time.Time) ([]byte, error)
- func (q *Queries) GetSessionByTokenHash(ctx context.Context, tokenHash []byte) (GetSessionByTokenHashRow, error)
- func (q *Queries) GetTagByName(ctx context.Context, arg GetTagByNameParams) (Tag, error)
- func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error)
- func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
- func (q *Queries) GetUserPermissions(ctx context.Context, arg GetUserPermissionsParams) ([]string, error)
- func (q *Queries) GetUserRoleInWorkspace(ctx context.Context, arg GetUserRoleInWorkspaceParams) (GetUserRoleInWorkspaceRow, error)
- func (q *Queries) GetWorkspaceDefaultDomain(ctx context.Context) (GetWorkspaceDefaultDomainRow, error)
- func (q *Queries) GetWorkspaceStats(ctx context.Context, arg GetWorkspaceStatsParams) ([]GetWorkspaceStatsRow, error)
- func (q *Queries) GetWorkspaceTotals(ctx context.Context, arg GetWorkspaceTotalsParams) (GetWorkspaceTotalsRow, error)
- func (q *Queries) IsAliasTaken(ctx context.Context, arg IsAliasTakenParams) (bool, error)
- func (q *Queries) ListAPIKeysForUser(ctx context.Context, arg ListAPIKeysForUserParams) ([]ListAPIKeysForUserRow, error)
- func (q *Queries) ListLinks(ctx context.Context, arg ListLinksParams) ([]ListLinksRow, error)
- func (q *Queries) ListPermissionSlugs(ctx context.Context) ([]string, error)
- func (q *Queries) ListTags(ctx context.Context, workspaceID uuid.UUID) ([]ListTagsRow, error)
- func (q *Queries) ListUserSessions(ctx context.Context, userID uuid.UUID) ([]ListUserSessionsRow, error)
- func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error)
- func (q *Queries) LockFirstUserSetup(ctx context.Context) error
- func (q *Queries) PurgeExpiredLinks(ctx context.Context, batchSize int32) ([]PurgeExpiredLinksRow, error)
- func (q *Queries) PurgeExpiredSalts(ctx context.Context) (int64, error)
- func (q *Queries) RecordFailedLogin(ctx context.Context, arg RecordFailedLoginParams) (RecordFailedLoginRow, error)
- func (q *Queries) RecordJobFailure(ctx context.Context, arg RecordJobFailureParams) error
- func (q *Queries) RecordSuccessfulLogin(ctx context.Context, id uuid.UUID) error
- func (q *Queries) ReserveAlias(ctx context.Context, arg ReserveAliasParams) error
- func (q *Queries) ResolveAliasForRedirect(ctx context.Context, arg ResolveAliasForRedirectParams) (ResolveAliasForRedirectRow, error)
- func (q *Queries) ResolveDefaultDomain(ctx context.Context) (ResolveDefaultDomainRow, error)
- func (q *Queries) ResolveDomainByHostname(ctx context.Context, lower string) (ResolveDomainByHostnameRow, error)
- func (q *Queries) RestoreLink(ctx context.Context, arg RestoreLinkParams) (Link, error)
- func (q *Queries) RevokeAPIKey(ctx context.Context, arg RevokeAPIKeyParams) (int64, error)
- func (q *Queries) RevokeAllUserSessions(ctx context.Context, arg RevokeAllUserSessionsParams) error
- func (q *Queries) RevokeSession(ctx context.Context, id uuid.UUID) error
- func (q *Queries) RollupDimensionDaily(ctx context.Context, arg RollupDimensionDailyParams) error
- func (q *Queries) RollupLinkDaily(ctx context.Context, arg RollupLinkDailyParams) error
- func (q *Queries) RollupWorkspaceDaily(ctx context.Context, arg RollupWorkspaceDailyParams) error
- func (q *Queries) SetDefaultDomainRootRedirect(ctx context.Context, rootRedirectUrl *string) (SetDefaultDomainRootRedirectRow, error)
- func (q *Queries) SetJobWatermark(ctx context.Context, arg SetJobWatermarkParams) error
- func (q *Queries) SetPrimaryDestination(ctx context.Context, arg SetPrimaryDestinationParams) error
- func (q *Queries) SoftDeleteLink(ctx context.Context, arg SoftDeleteLinkParams) (SoftDeleteLinkRow, error)
- func (q *Queries) TouchAPIKeys(ctx context.Context, arg TouchAPIKeysParams) error
- func (q *Queries) TouchSession(ctx context.Context, id uuid.UUID) error
- func (q *Queries) UpdateDestinationURL(ctx context.Context, arg UpdateDestinationURLParams) error
- func (q *Queries) UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error)
- func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
- func (q *Queries) WithTx(tx pgx.Tx) *Queries
- type RecordFailedLoginParams
- type RecordFailedLoginRow
- type RecordJobFailureParams
- type ReserveAliasParams
- type ReservedAlias
- type ResolveAliasForRedirectParams
- type ResolveAliasForRedirectRow
- type ResolveDefaultDomainRow
- type ResolveDomainByHostnameRow
- type RestoreLinkParams
- type RevokeAPIKeyParams
- type RevokeAllUserSessionsParams
- type Role
- type RolePermission
- type RollupDimensionDailyParams
- type RollupLinkDailyParams
- type RollupWorkspaceDailyParams
- type RoutingRule
- type Session
- type SetDefaultDomainRootRedirectRow
- type SetJobWatermarkParams
- type SetPrimaryDestinationParams
- type SoftDeleteLinkParams
- type SoftDeleteLinkRow
- type Tag
- type TouchAPIKeysParams
- type UpdateDestinationURLParams
- type UpdateLinkParams
- type UpdateUserPasswordParams
- type User
- type Visitor
- type Webhook
- type WebhookDelivery
- type Workspace
- type WorkspaceClickDaily
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AnalyticsSalt ¶
type AttachTagParams ¶
type AutomationRule ¶
type ClickEvent ¶
type CountLinksParams ¶
type CreateAPIKeyParams ¶
type CreateDestinationParams ¶
type CreateLinkParams ¶
type CreateMembershipParams ¶
type CreateSaltParams ¶
type CreateSessionParams ¶
type CreateTagParams ¶
type CreateUserParams ¶
type CreateWorkspaceParams ¶
type Destination ¶
type GetAPIKeyByPrefixRow ¶
type GetLinkByAliasParams ¶
type GetLinkDimensionsParams ¶
type GetLinkDimensionsRow ¶
type GetLinkStatsParams ¶
type GetLinkStatsRow ¶
type GetRecentClicksParams ¶
type GetRecentClicksRow ¶
type GetTagByNameParams ¶
type GetWorkspaceStatsParams ¶
type GetWorkspaceStatsRow ¶
type GetWorkspaceTotalsRow ¶
type IsAliasTakenParams ¶
type Link ¶
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
}
type LinkClickDaily ¶
type LinkDimensionDaily ¶
type ListAPIKeysForUserRow ¶
type ListLinksParams ¶
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
TagNames []string
TagIds []string
}
type ListTagsRow ¶
type ListUserSessionsRow ¶
type ListUsersRow ¶
type Membership ¶
type Notification ¶
type Organization ¶
type PurgeExpiredLinksRow ¶
type Querier ¶
type Querier interface {
ArchiveLink(ctx context.Context, arg ArchiveLinkParams) (Link, error)
AttachTag(ctx context.Context, arg AttachTagParams) error
CountClickEvents(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)
// Users, sessions and tenancy provisioning.
// Drives the first-run setup flow: /setup exists only while this is zero.
CountUsers(ctx context.Context) (int64, error)
// API keys and the permission vocabulary their scopes are drawn from.
CreateAPIKey(ctx context.Context, arg CreateAPIKeyParams) (ApiKey, error)
CreateDestination(ctx context.Context, arg CreateDestinationParams) (Destination, error)
// Links, destinations and tags.
CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error)
CreateMembership(ctx context.Context, arg CreateMembershipParams) (Membership, error)
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, 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)
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)
CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, 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)
// 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)
DeleteTag(ctx context.Context, arg DeleteTagParams) (int64, error)
DetachAllTags(ctx context.Context, linkID 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.
GetAPIKeyByPrefix(ctx context.Context, prefix string) (GetAPIKeyByPrefixRow, error)
// The instance's link domain and where its root points. Phase 1 has exactly one
// default domain; Phase 2 gives a workspace its own and this gains a filter.
GetDefaultDomainSettings(ctx context.Context) (GetDefaultDomainSettingsRow, error)
// The workspace a user lands in with no explicit selection. Ordered so the
// result is deterministic rather than whatever the planner returns first.
GetDefaultWorkspaceForUser(ctx context.Context, userID uuid.UUID) (Workspace, 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)
// 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)
// 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)
// 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)
// 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)
GetWorkspaceDefaultDomain(ctx context.Context) (GetWorkspaceDefaultDomainRow, 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)
// 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)
// 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.
ListAPIKeysForUser(ctx context.Context, arg ListAPIKeysForUserParams) ([]ListAPIKeysForUserRow, 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)
// 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)
// 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)
ListUserSessions(ctx context.Context, userID uuid.UUID) ([]ListUserSessionsRow, error)
ListUsers(ctx context.Context) ([]ListUsersRow, 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 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)
// 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.
RecordJobFailure(ctx context.Context, arg RecordJobFailureParams) error
RecordSuccessfulLogin(ctx context.Context, id uuid.UUID) 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 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.
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)
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)
// 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
RevokeSession(ctx context.Context, id uuid.UUID) 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
// NULL clears it, which restores the 404 the root answered before anyone set
// anything.
SetDefaultDomainRootRedirect(ctx context.Context, rootRedirectUrl *string) (SetDefaultDomainRootRedirectRow, error)
SetJobWatermark(ctx context.Context, arg SetJobWatermarkParams) error
SetPrimaryDestination(ctx context.Context, arg SetPrimaryDestinationParams) 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)
// 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
// The trigger on destinations mirrors this into links.primary_url, so the hot
// path never joins.
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)
UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
}
type Queries ¶
type Queries struct {
// contains filtered or unexported fields
}
func (*Queries) ArchiveLink ¶
func (*Queries) AttachTag ¶
func (q *Queries) AttachTag(ctx context.Context, arg AttachTagParams) error
func (*Queries) CountClickEvents ¶
func (*Queries) CountLinks ¶
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) CountUsers ¶
Users, sessions and tenancy provisioning. Drives the first-run setup flow: /setup exists only while this is zero.
func (*Queries) CreateAPIKey ¶
API keys and the permission vocabulary their scopes are drawn from.
func (*Queries) CreateDestination ¶
func (q *Queries) CreateDestination(ctx context.Context, arg CreateDestinationParams) (Destination, error)
func (*Queries) CreateLink ¶
Links, destinations and tags.
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) CreateSalt ¶
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 (*Queries) CreateTag ¶
--- tags -------------------------------------------------------------------
func (*Queries) CreateUser ¶
func (*Queries) CreateWorkspace ¶
func (*Queries) DeleteExpiredSessions ¶
Reaper. Revoked rows are kept briefly so "sign out everywhere" is visible in the session list before it disappears.
func (*Queries) DeleteRevokedAPIKeys ¶
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) DetachAllTags ¶
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.
func (*Queries) GetDefaultDomainSettings ¶
func (q *Queries) GetDefaultDomainSettings(ctx context.Context) (GetDefaultDomainSettingsRow, error)
The instance's link domain and where its root points. Phase 1 has exactly one default domain; Phase 2 gives a workspace its own and this gains a filter.
func (*Queries) GetDefaultWorkspaceForUser ¶
func (q *Queries) GetDefaultWorkspaceForUser(ctx context.Context, userID uuid.UUID) (Workspace, error)
The workspace a user lands in with no explicit selection. Ordered so the result is deterministic rather than whatever the planner returns first.
func (*Queries) GetJobWatermark ¶
--- 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 (*Queries) GetLink ¶
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 (*Queries) GetLinkDimensions ¶
func (q *Queries) GetLinkDimensions(ctx context.Context, arg GetLinkDimensionsParams) ([]GetLinkDimensionsRow, error)
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 (*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 (*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 (*Queries) GetUserByEmail ¶
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 (*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) GetWorkspaceDefaultDomain ¶
func (q *Queries) GetWorkspaceDefaultDomain(ctx context.Context) (GetWorkspaceDefaultDomainRow, error)
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) IsAliasTaken ¶
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) ListAPIKeysForUser ¶
func (q *Queries) ListAPIKeysForUser(ctx context.Context, arg ListAPIKeysForUserParams) ([]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.
func (*Queries) ListLinks ¶
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) ListPermissionSlugs ¶
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) ListTags ¶
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) ListUserSessions ¶
func (*Queries) ListUsers ¶
func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error)
func (*Queries) LockFirstUserSetup ¶
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 (*Queries) PurgeExpiredLinks ¶
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 ¶
The de-identification step. Once the salt is gone the day's hashes cannot be linked back to an address.
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.
func (*Queries) RecordSuccessfulLogin ¶
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) 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.
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) RestoreLink ¶
func (*Queries) RevokeAPIKey ¶
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) 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) RevokeSession ¶
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) 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) SetJobWatermark ¶
func (q *Queries) SetJobWatermark(ctx context.Context, arg SetJobWatermarkParams) error
func (*Queries) SetPrimaryDestination ¶
func (q *Queries) SetPrimaryDestination(ctx context.Context, arg SetPrimaryDestinationParams) error
func (*Queries) SoftDeleteLink ¶
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) 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 ¶
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 (*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.
func (*Queries) UpdateLink ¶
COALESCE with sqlc.narg gives partial update: a NULL argument leaves the column alone, so PATCH semantics need no dynamic SQL.
func (*Queries) UpdateUserPassword ¶
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
type RecordFailedLoginParams ¶
type RecordFailedLoginRow ¶
type RecordJobFailureParams ¶
type ReserveAliasParams ¶
type ReservedAlias ¶
type ResolveDefaultDomainRow ¶
type RollupLinkDailyParams ¶
type RoutingRule ¶
type SetJobWatermarkParams ¶
type SoftDeleteLinkParams ¶
type SoftDeleteLinkRow ¶
type UpdateLinkParams ¶
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
}