models

package
v0.0.0-...-b82ae73 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package models provides database model definitions for SolidPing.

Package models provides data models for the SolidPing database.

Index

Constants

View Source
const (
	// AgentStatusActive is an enrolled, usable agent.
	AgentStatusActive = "active"
	// AgentStatusRevoked is an agent an admin has revoked; it can no longer
	// authenticate and its X25519 key is excluded from future seals.
	AgentStatusRevoked = "revoked"
)

Agent status values.

View Source
const (
	// AgentKindOrg is a tenant-private deported agent: bound to exactly one
	// organization and one `@<org>/<region>` private region.
	AgentKindOrg = "org"
	// AgentKindSystem is a platform-operated agent (e.g. a fly.io machine)
	// serving a SHARED cloud region across every organization. It has no owning
	// org, and its claim scope is the region alone — the same scope the
	// in-cluster DirectBackend uses.
	AgentKindSystem = "system"
)

Agent kinds (spec 2026-07-27-01).

View Source
const (
	DeviceAuthStatusPending  = "pending"
	DeviceAuthStatusApproved = "approved"
	DeviceAuthStatusDenied   = "denied"
)

Device authorization request statuses (RFC 8628 §3.3-3.5). A request starts pending and moves exactly once to approved or denied; the row is then consumed (hard-deleted) by the client's next poll.

View Source
const (
	// EmailSuppressionSourceLink means the recipient followed the
	// unsubscribe link (either the RFC 8058 one-click POST or the GET
	// confirmation page).
	EmailSuppressionSourceLink = "link"
	// EmailSuppressionSourceHeader means an inbox provider auto-submitted the
	// List-Unsubscribe-Post one-click request on the recipient's behalf
	// without them visiting the confirmation page. Same code path as "link"
	// today (the POST handler cannot distinguish the two) — kept as a
	// distinct value for when/if that becomes distinguishable, and so the
	// audit trail's vocabulary already has a slot for it.
	EmailSuppressionSourceHeader = "header"
	// EmailSuppressionSourceDashboard means an org admin added the
	// suppression manually from the dashboard (not currently exposed by the
	// API — creation is always recipient-initiated — but modeled so a future
	// admin-initiated mute doesn't need a schema change).
	EmailSuppressionSourceDashboard = "dashboard"
)

Email suppression source constants — where the suppression came from.

View Source
const (
	ResolutionTypeAuto    = "auto"
	ResolutionTypeManual  = "manual"
	ResolutionTypeExpired = "expired"
)

Resolution type values for the resolution_type column. Manual = closed by a human action; auto = the adaptive resolution logic decided the check is up again; expired = future use for time-based resolution.

View Source
const (
	// IncidentKindCheck is a failing check — the original and default meaning,
	// so every pre-existing row keeps it with no backfill.
	IncidentKindCheck = "check"
	// IncidentKindSLOBurn is an SLO error-budget burn-rate alert (spec
	// 2026-08-21-08). It carries SLOUID + SLOAlertPolicyUID, and a
	// representative check in CheckUID purely so channel resolution and
	// escalation-policy resolution have an anchor to work from.
	IncidentKindSLOBurn = "slo_burn"
)

Incident kinds. `kind` discriminates what an incident row is ABOUT. It is a discriminator, not a category: everything downstream (notification fan-out, escalation policies, ack/snooze/resolve, the timeline) is deliberately identical for both, which is the whole reason burn alerts are incidents rather than a parallel object.

View Source
const (
	IncidentNotificationStatusPending  = "pending"
	IncidentNotificationStatusSent     = "sent"
	IncidentNotificationStatusFailed   = "failed"
	IncidentNotificationStatusCanceled = "cancelled" //nolint:misspell // DB column uses British English
	IncidentNotificationStatusSkipped  = "skipped"
)

Status constants for IncidentNotification.

View Source
const (
	IncidentNotificationSourceCheckConnection      = "check_connection"
	IncidentNotificationSourceEscalationUser       = "escalation_user"
	IncidentNotificationSourceEscalationSchedule   = "escalation_schedule"
	IncidentNotificationSourceEscalationAllAdmins  = "escalation_all_admins"
	IncidentNotificationSourceEscalationConnection = "escalation_connection"
)

Source constants for IncidentNotification.

View Source
const (
	// SlackCommentIngestionExplicit ingests only `/comment` invocations.
	SlackCommentIngestionExplicit = "explicit"
	// SlackCommentIngestionAll ingests every human thread reply.
	SlackCommentIngestionAll = "all"
)

Slack comment-ingestion modes. See SlackSettings.CommentIngestion.

View Source
const (
	// DiscordCommentIngestionExplicit ingests only `/solidping comment`.
	DiscordCommentIngestionExplicit = "explicit"
	// DiscordCommentIngestionAll ingests every human thread reply.
	DiscordCommentIngestionAll = "all"
)

Discord comment-ingestion modes. Deliberately the same string values as the Slack ones so an operator reading two settings blobs sees one vocabulary.

View Source
const (
	FreeboxStatusPairing = "pairing"
	FreeboxStatusGranted = "granted"
	FreeboxStatusDenied  = "denied"
	FreeboxStatusTimeout = "timeout"
)

Freebox pairing status values that live in FreeboxSettings.Status. They are declared here (rather than in the integrations/freebox package) so model callers — channel forms, list responses, audit logs — can branch on them without importing the higher-level integration package.

View Source
const (
	// RecurrenceNone marks a one-off (non-recurring) window.
	RecurrenceNone = "none"
	// RecurrenceDaily repeats the window every day at the anchor's time-of-day.
	RecurrenceDaily = "daily"
	// RecurrenceWeekly repeats the window every week on the anchor's weekday.
	RecurrenceWeekly = "weekly"
	// RecurrenceMonthly repeats the window every month on the anchor's day-of-month.
	RecurrenceMonthly = "monthly"
)

Maintenance window recurrence cadences.

View Source
const (
	// MaintenanceStatusActive means a window occurrence covers now.
	MaintenanceStatusActive = "active"
	// MaintenanceStatusUpcoming means the window has future occurrences but none cover now.
	MaintenanceStatusUpcoming = "upcoming"
	// MaintenanceStatusPast means the window has no remaining occurrences.
	MaintenanceStatusPast = "past"
)

Maintenance window lifecycle statuses (returned by Status).

View Source
const (
	// UsageCounterKindSMS counts outbound SMS messages per month.
	UsageCounterKindSMS = "sms"
	// UsageCounterKindVoice counts outbound voice calls per month.
	UsageCounterKindVoice = "voice"
	// UsageCounterKindWhatsApp counts outbound WhatsApp template messages per
	// month.
	UsageCounterKindWhatsApp = "whatsapp"
	// UsageCounterKindCheckRateLimited counts check executions that were
	// deferred because the org exhausted its MaxChecksPerMinute budget. Unlike
	// the three kinds above it is a DAILY counter: PeriodStart is the UTC day,
	// because "were executions skipped today?" is the question the over-limit
	// banner answers, and a monthly bucket would keep the banner lit for weeks
	// after the org came back under its cap (spec 2026-08-26-03).
	UsageCounterKindCheckRateLimited = "check_rate_limited"
)

Usage-counter kinds.

View Source
const (
	// ReportFrequencyWeekly emits one report per ISO week (Monday start).
	ReportFrequencyWeekly = "weekly"
	// ReportFrequencyMonthly emits one report per calendar month.
	ReportFrequencyMonthly = "monthly"
)

Report schedule frequencies.

View Source
const (
	PeriodTypeRaw   = "raw"
	PeriodTypeHour  = "hour"
	PeriodTypeDay   = "day"
	PeriodTypeMonth = "month"
)

PeriodType values for the Result.PeriodType column.

View Source
const (
	// AbandonedResultLeaseGrace mirrors the check_jobs lease formula
	// (scheduled_at + period + 30s, see checkjobsvc.Service.ClaimJobs): the
	// worst-case slack a legitimately in-flight attempt could still need on
	// top of the check's own period.
	AbandonedResultLeaseGrace = 30 * time.Second

	// AbandonedResultMultiplier pads (period + lease grace) generously so a
	// single routine worker restart mid-cycle is never mistaken for
	// abandonment. Only a check that has had no chance to produce ANY result
	// across several of its own periods is reaped.
	AbandonedResultMultiplier = 5
)

Constants behind AbandonedResultThreshold — see its doc comment.

View Source
const (
	// SLOAlertPolicyKindFast is the "we are about to spend the month in an
	// afternoon" policy: a short long-window and a high threshold.
	SLOAlertPolicyKindFast = "fast"
	// SLOAlertPolicyKindSlow is the "this has been quietly eating the budget
	// all morning" policy: a longer window and a lower threshold.
	SLOAlertPolicyKindSlow = "slow"
)

SLO alert policy kinds. There are exactly two built-ins — the product ships them, operators tune them, nobody invents a third from the API.

View Source
const (
	// SLOAlertSeverityCritical is the fast-burn default.
	SLOAlertSeverityCritical = "critical"
	// SLOAlertSeverityWarning is the slow-burn default.
	SLOAlertSeverityWarning = "warning"
)

Alert severities. Not stored on the incident — this codebase attaches severity to escalation-policy steps, not to incidents — but carried into the incident title, details and every notification body so the person paged can tell a fast burn from a slow one without opening the dashboard.

View Source
const (
	StatusPageVisibilityPublic   = "public"
	StatusPageVisibilityPrivate  = "private"
	StatusPageVisibilityPassword = "password"
)

Status page visibility values. `public` is world-readable, `private` is fully hidden (the public endpoints 404 as if it did not exist), and `password` is shared-with-a-secret: the public endpoints answer 401 until the visitor unlocks the page (spec 2026-08-21-07).

View Source
const (
	// CustomDomainStateNone means no custom domain is configured.
	CustomDomainStateNone = "none"
	// CustomDomainStatePending means a domain is configured but has never
	// verified. The sweep NEVER auto-promotes out of this state — a first
	// verification is an operator action, so a hostname someone else parked a
	// CNAME on cannot bootstrap itself into being served.
	CustomDomainStatePending = "pending"
	// CustomDomainStateActive means verified and serving.
	CustomDomainStateActive = "active"
	// CustomDomainStateGrace means still verified and STILL SERVING, but
	// re-verification is currently failing. The common case (a transient DNS
	// fault) lives and dies here without a visitor ever noticing.
	CustomDomainStateGrace = "grace"
	// CustomDomainStateDemoted means the domain stayed unreachable well past
	// the grace window and verification was cleared. Recoverable: the sweep
	// re-promotes it after CustomDomainRepromoteSuccesses consecutive
	// successes while a valid certificate is still held.
	CustomDomainStateDemoted = "demoted"
)

Custom-domain lifecycle states. Stored in status_pages.custom_domain_state.

The states are ordered by trust: none -> pending -> active, with grace as the "still serving but the re-checks are failing" holding pen and demoted as the terminal-until-recovered state. ONLY `demoted` has custom_domain_verified_at = NULL; `grace` keeps it set, and that is precisely what makes a DNS blip invisible to the page's visitors.

View Source
const (
	// DefaultAvailabilityThresholdUp is the green floor: pct >= this is "up".
	DefaultAvailabilityThresholdUp = 99.9
	// DefaultAvailabilityThresholdDegraded is the amber floor: pct >= this
	// (and below the up threshold) is "degraded"; below it is "down".
	DefaultAvailabilityThresholdDegraded = 99.0
)

Default availability color thresholds (spec 2026-08-03-01), used whenever a page's settings (or a section within it) don't specify a value. Badges (internal/handlers/badges) intentionally stay on these same numeric defaults without reading page settings — see that package's Decisions.

View Source
const (
	// WireStatusCreated is the wire name for a just-created, not-yet-executed status.
	WireStatusCreated = "created"
	// WireStatusUp is the wire name for a healthy status.
	WireStatusUp = "up"
	// WireStatusDown is the wire name for a failing status.
	WireStatusDown = "down"
	// WireStatusValidating is the wire name for the transient pre-incident status.
	WireStatusValidating = "validating"
	// WireStatusDegraded is the wire name for the aggregated/summary degraded status.
	WireStatusDegraded = "degraded"
	// WireStatusWarning is the wire name for the live "up with something to report" status.
	WireStatusWarning = "warning"
	// WireStatusTimeout is the wire name for a timed-out status.
	WireStatusTimeout = "timeout"
	// WireStatusError is the wire name for an errored status.
	WireStatusError = "error"
	// WireStatusRunning is the wire name for an in-progress status.
	WireStatusRunning = "running"
	// WireStatusAbandoned is the wire name for a raw attempt the
	// abandoned-result reaper finalized: terminal, but excluded from every
	// availability calculation (spec 2026-08-18-10).
	WireStatusAbandoned = "abandoned"
	// WireStatusUnknown is the fallback wire name for an unrecognized status.
	WireStatusUnknown = "unknown"
)

Lowercase status wire names, the single source of truth for the string form of CheckStatus (and the matching subtest/table labels in tests). These are the values the dashboard keys status colors and labels on. Centralizing them keeps any single literal from repeating across the package (goconst).

View Source
const (
	SupportChannelWhatsApp = "whatsapp"
	SupportChannelTelegram = "telegram"
	SupportChannelSMS      = "sms"
	SupportChannelSlack    = "slack"
	SupportChannelDiscord  = "discord"
	SupportChannelEmail    = "email"
)

Support channels. These are the inbound surfaces a human can reach us on. SupportChannelEmail exists in the vocabulary (and in the database CHECK constraint) but nothing writes it in v1: inbound email capture is deliberately a separate, later spec, so v1 ships the asymmetry "email support is a human mailbox, not a thread in the inbox". Reserving the value now means that spec needs no migration.

View Source
const (
	SupportStatusOpen    = "open"
	SupportStatusPending = "pending"
	SupportStatusClosed  = "closed"
)

Thread status. Set by the OPERATOR, deliberately — this is NOT the reply window. See SupportThread.ReplyWindow.

View Source
const (
	SupportDirectionInbound  = "inbound"
	SupportDirectionOutbound = "outbound"
)

Message direction.

View Source
const (
	SupportRawTypeText        = "text"
	SupportRawTypeImage       = "image"
	SupportRawTypeAudio       = "audio"
	SupportRawTypeVideo       = "video"
	SupportRawTypeDocument    = "document"
	SupportRawTypeLocation    = "location"
	SupportRawTypeSticker     = "sticker"
	SupportRawTypeUnsupported = "unsupported"
)

Raw message types. Anything the channel sends that is not plain text is recorded with a placeholder body and the real kind here, so an operator can see that a person sent a photo rather than seeing nothing at all.

View Source
const (
	UserContactTypeEmail     = "email"
	UserContactTypePhone     = "phone"
	UserContactTypeSlackUser = "slack_user"
	UserContactTypePushover  = "pushover_user"
	UserContactTypeNtfy      = "ntfy_topic"
	UserContactTypeWebPush   = "webpush"
	// UserContactTypeWhatsApp is a WhatsApp-reachable number. Deliberately
	// DISTINCT from UserContactTypePhone even when the digits are identical:
	// an SMS-verified number proves nothing about WhatsApp reachability, and
	// the WhatsApp verification round-trip doubles as Meta's required record
	// of the user's opt-in to receive business-initiated messages. Collapsing
	// the two would silently reuse an SMS consent as a WhatsApp consent.
	UserContactTypeWhatsApp = "whatsapp"
	// UserContactTypeTelegram is a Telegram chat id the instance bot can message.
	// The stored Value is the numeric chat id, never a @username: usernames can
	// be changed or reassigned, chat ids cannot.
	//
	// Deliberately NOT in VerifiableContactTypes: there is no code round-trip.
	// Pressing Start in Telegram (which delivers /start <token> to our webhook)
	// IS the proof of reachability and the opt-in, so the contact is created
	// already verified — and, correspondingly, a telegram contact may never be
	// created through the generic POST /notification-contacts endpoint, since
	// nothing there would stop a user typing a stranger's chat id.
	UserContactTypeTelegram = "telegram"
)

UserContact type vocabulary.

View Source
const (
	// IdentitySourceAuto marks an identity created by the email auto-match.
	IdentitySourceAuto = "auto"
	// IdentitySourceManual marks an identity an admin picked explicitly.
	IdentitySourceManual = "manual"
)

Identity source vocabulary. `auto` rows were produced by the email auto-match (Slack `users.lookupByEmail`); `manual` rows were picked by an admin. A re-sync never overwrites a `manual` row — the admin's choice is the stronger statement about who this person is.

View Source
const (
	// CapabilityIPv4 means the worker can originate IPv4 traffic.
	CapabilityIPv4 = "ipv4"
	// CapabilityIPv6 means the worker can originate IPv6 traffic.
	CapabilityIPv6 = "ipv6"
	// CapabilityBrowser means the worker can actually run a `browser` check:
	// a reachable remote Chrome (CDP) endpoint, or a local Chrome/Chromium
	// binary. Self-probed like the egress families, and advisory in the same
	// way — it drives a creation-time warning, never scheduling.
	CapabilityBrowser = "browser"
)

Capability names, as stored verbatim in workers.capabilities and re-exported by internal/regions. They live here because they are the values the column holds, and because a producer (the worker self-probe, the agent claim path) must be able to name one without importing the region service.

Names are lowercase `[a-z0-9-]+` slugs; the database CHECK constraint and the SQLite triggers reject anything else.

View Source
const CheckConfigKeyTimeout = "timeout"

CheckConfigKeyTimeout is the check-config key holding the optional per-check execution timeout, stored as a Go duration string (spec 2026-07-11-05).

View Source
const DefaultAutoPublishDelaySeconds = 60

DefaultAutoPublishDelaySeconds is the debounce a new page starts with: an incident must still be open a minute later before customers hear about it.

View Source
const DefaultPublicationNotifyCap = 4

DefaultPublicationNotifyCap is how many subscriber fan-out waves one publication may trigger per hour before further updates are posted silently. Overridable per org via the `status_page.publication_notify_cap` parameter.

View Source
const DefaultReportTimezone = "UTC"

DefaultReportTimezone is the timezone a new schedule gets when none is given.

View Source
const DefaultSLOAlertMinSamples = 3

DefaultSLOAlertMinSamples is the per-window probe floor below which a window is inconclusive.

Deliberately low. A check whose period exceeds shortWindow/minSamples can never satisfy its own short window, and an alert policy that silently never fires is worse than one that occasionally fires on thin evidence — the operator can see and raise this number, but cannot see an alert that never happened.

View Source
const DefaultSLOTimezone = "UTC"

DefaultSLOTimezone is the timezone a new SLO gets when the caller supplies none.

View Source
const DeliveryDetailsBodyCap = 16 * 1024

DeliveryDetailsBodyCap is the maximum number of bytes retained for a captured request or response body before persistence. Bodies larger than this are truncated; the channel sender is responsible for capping before it builds the DeliveryDetails so secrets in oversized bodies are never marshaled.

View Source
const EntitlementsPayloadVersion = 1

EntitlementsPayloadVersion is the current schema version for the payload column. Future shape-breaking changes bump this and add a branch in EntitlementsPayload.UnmarshalJSON.

View Source
const EventTypeLikeEscape = `\`

EventTypeLikeEscape is the escape character the event-type family predicates pair with LIKE. It must be spelled the same way in every dialect's query.

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

HeartbeatCounterTTL is how long a heartbeat check's SP2 replay counter is kept after the last accepted beat.

It is a SLIDING window: every accepted signed beat pushes it out again, so a device that is actually beating never expires its own counter. Only a check that has stopped beating for this long lets its counter be swept, which is what keeps the store from accumulating counters for checks nobody uses any more.

Defined here, once, because both dialects bind it as a parameter — a duplicated interval literal in two .go files is exactly the kind of thing that drifts.

View Source
const IncidentNotificationChannelTypeNone = "none"

IncidentNotificationChannelTypeNone is used for skipped rows where no channel is involved.

View Source
const ParamKeyTracerouteEnabled = "diagnostics.traceroute.enabled"

ParamKeyTracerouteEnabled is the ORG-scoped switch that supplies the default for every check whose own `traceroute_on_failure` is NULL (spec 2026-08-21-10). Absent means ON.

It lives here, next to the parameter machinery, rather than in the incident package that reads it: the org-settings handler writes it and the incident pipeline reads it, and those two packages sit on opposite sides of an import cycle. A shared constant is what keeps the two ends from drifting apart into two subtly different strings.

View Source
const ParameterValueKey = "value"

ParameterValueKey is the single JSON key a scalar parameter value is stored under. Both engines write and read `{"value": …}`; naming it once keeps them from ever drifting.

View Source
const SectionSelectorMaxLabels = 10

SectionSelectorMaxLabels caps how many key=value pairs one selector may carry. Each pair becomes its own correlated subquery in ListChecks, so the cap is what stops a single section from authoring an arbitrarily expensive query.

View Source
const SectionSelectorMaxValueLen = 200

SectionSelectorMaxValueLen caps a selector label value, matching the label authoring cap in the dashboard (label-shared.ts VALUE_MAX).

View Source
const SlackScopeIMHistory = "im:history"

SlackScopeIMHistory is the bot scope Slack requires before it will deliver `message.im` — a direct message to the bot. Without it a DM is not merely ignored, it never arrives.

View Source
const SupportBodyMaxLength = 8000

SupportBodyMaxLength caps a stored message body. These endpoints are fed by publicly reachable phone numbers, so the body is attacker-influenced: over the cap it is truncated and flagged rather than rejected, because a truncated record still beats a lost one.

View Source
const WhatsAppReplyWindow = 24 * time.Hour

WhatsAppReplyWindow is the free customer-service window Meta opens when a user messages us. Inside it we may reply with ordinary text; outside it only an approved template may be sent, so a free-form reply is *impossible* rather than merely discouraged.

Variables

View Source
var (
	// ErrRecentResultsNoOrganization is returned when the filter names no
	// organization — every index on `results` leads with organization_uid.
	ErrRecentResultsNoOrganization = errors.New("recent results: organization uid is required")
	// ErrRecentResultsNoTiers is returned when the filter names no tier branch.
	ErrRecentResultsNoTiers = errors.New("recent results: at least one tier is required")
	// ErrRecentResultsMixedTier is returned when one branch names both raw and
	// a rollup tier (or names none at all).
	//
	// This is the guard that makes spec 2026-08-22-05's worst failure mode
	// unwritable. A per-check LATERAL/correlated fetch WITHOUT a tier
	// predicate does not fall back to the old plan — it becomes one sequential
	// scan of `results` PER CHECK, measured at 12 274 ms for a 20-check page
	// against 662 ms for the query it replaced. Both partial indexes on
	// `results` split on `period_type = 'raw'`, so a branch straddling the
	// split is implied by neither and can only be a scan.
	ErrRecentResultsMixedTier = errors.New(
		"recent results: each tier must sit entirely on one side of the raw/rollup index split")
	// ErrRecentResultsNoSince is returned when a tier carries no lower bound.
	ErrRecentResultsNoSince = errors.New("recent results: each tier needs a period_start lower bound")
	// ErrRecentResultsNoLimit is returned when no usable per-check budget is set.
	ErrRecentResultsNoLimit = errors.New("recent results: a positive default per-check limit is required")
)

Errors returned by RecentResultsPerCheckFilter.Validate.

View Source
var (
	// ErrSelectorEmpty is returned for `{}` — neither `all` nor `labels`.
	ErrSelectorEmpty = errors.New("selector must set either all or labels")
	// ErrSelectorAmbiguous is returned when both `all` and `labels` are set.
	ErrSelectorAmbiguous = errors.New("selector cannot set both all and labels")
	// ErrSelectorLabelsEmpty is returned for an empty `labels` object, which
	// would silently mean "every check" — the caller must say `all` for that.
	ErrSelectorLabelsEmpty = errors.New("selector labels must not be empty")
	// ErrSelectorTooManyLabels is returned above SectionSelectorMaxLabels.
	ErrSelectorTooManyLabels = errors.New("selector has too many labels")
	// ErrSelectorLabelKeyInvalid is returned for a key that no label could have.
	ErrSelectorLabelKeyInvalid = errors.New("selector label key is invalid")
	// ErrSelectorLabelValueInvalid is returned for an empty or over-long value.
	// Existence-only matching ("*") is deliberately NOT supported in v1.
	ErrSelectorLabelValueInvalid = errors.New("selector label value is invalid")
)

Selector validation errors. They are returned to the API layer, which maps them onto VALIDATION_ERROR.

View Source
var ErrConflictingUserLimitKeys = errors.New(
	"maxUsers and maxSsoUsers are mutually exclusive; send only maxUsers",
)

ErrConflictingUserLimitKeys is returned when a payload sends both the canonical maxUsers key and its deprecated maxSsoUsers alias at once.

View Source
var ErrDeliveryDetailsScanType = errors.New("unsupported type for DeliveryDetails.Scan")

ErrDeliveryDetailsScanType is returned when DeliveryDetails.Scan receives a value of an unexpected SQL type.

View Source
var ErrInvalidCapabilitySet = errors.New("invalid capability set")

ErrInvalidCapabilitySet is returned by ValidateCapabilitySet.

View Source
var ErrStatusPageSettingsScanType = errors.New("unsupported type for StatusPageSettings.Scan")

ErrStatusPageSettingsScanType is returned when StatusPageSettings.Scan receives a value of an unexpected SQL type.

View Source
var ErrUnknownEntitlementsPayloadVersion = errors.New("unknown entitlements payload version")

ErrUnknownEntitlementsPayloadVersion is returned when the payload JSON's version discriminator does not match a known shape. Callers can use errors.Is to detect it.

Functions

func AbandonedResultThreshold

func AbandonedResultThreshold(checkPeriod time.Duration) time.Duration

AbandonedResultThreshold returns how old a lifecycle-marker raw row must be, relative to its check's period, before the abandoned-result reaper may finalize it: AbandonedResultMultiplier * (period + AbandonedResultLeaseGrace). This is "the check's period plus the worker lease timeout, with a generous multiplier" from spec 2026-08-18-03's Proposal.

func CapBody

func CapBody(body []byte) string

CapBody truncates a body to DeliveryDetailsBodyCap bytes, appending a marker when truncation occurred so the operator knows the stored copy is partial.

func ContactRequiresVerification

func ContactRequiresVerification(contactType string) bool

ContactRequiresVerification reports whether a contact type must complete the verification code round-trip before it can be used.

func EventTypeLikePattern

func EventTypeLikePattern(prefix string) string

EventTypeLikePattern turns a family prefix ("auth", or "auth." — both are accepted) into the SQL LIKE pattern matching every type in that family.

The escaping is not paranoia: real family names contain `_`, which LIKE treats as "any single character". Without it, a filter on "oncall_schedule" would also admit a hypothetical "oncallXschedule.*" family, and the *exclusion* used to hide auth events from non-admins would be the dangerous direction of the same bug.

func HideBrandingSectionPatch

func HideBrandingSectionPatch(hide bool) string

HideBrandingSectionPatch returns the `branding`-rooted merge patch that flips ONLY the white-label opt-in, leaving the two asset keys alone — what Postgres concatenates onto `settings->'branding'`. It is the generic PATCH path's write (StatusPageUpdate.HideBranding).

func HideBrandingSettingsPatch

func HideBrandingSettingsPatch(hide bool) string

HideBrandingSettingsPatch returns the same patch rooted at `settings` — what SQLite hands to json_patch.

func IsActiveAt

func IsActiveAt(window *MaintenanceWindow, target time.Time) bool

IsActiveAt determines whether a maintenance window is active at the given time.

func IsValidReportFrequency

func IsValidReportFrequency(frequency string) bool

IsValidReportFrequency reports whether a frequency string is accepted.

func LikeContainsPattern

func LikeContainsPattern(value string) string

LikeContainsPattern builds the SQL LIKE pattern matching any value that CONTAINS the given text, escaping the LIKE metacharacters so a user typing "100%" or "check_1" searches for those characters rather than for a wildcard. Pair it with EventTypeLikeEscape.

func MigrateRegionList

func MigrateRegionList(list []string, from, target string) ([]string, bool)

MigrateRegionList rewrites a check's region list, replacing every occurrence of `from` with `to` while preserving order and de-duplicating: a check that already declares BOTH slugs (the half-migrated case) must not end up with `to` twice, because the resulting check_jobs would violate the unique (check_uid, region) index.

The second return reports whether anything actually changed, so a caller can skip a no-op UPDATE — which is what makes the region migration idempotent.

Pure and allocation-light on purpose: it lives on the model package so both SQL dialects can share one definition of "what the rewritten array is", rather than expressing it twice in dialect-specific SQL.

func RawAvailability

func RawAvailability(results []*Result) (int, int)

RawAvailability computes (successCount, countableTotal) over raw results, skipping lifecycle markers and reaped/abandoned attempts. Callers derive pct = 100*success/total when total > 0.

func ResultColumnsWithoutBlobs

func ResultColumnsWithoutBlobs(alias string) []string

ResultColumnsWithoutBlobs returns every persisted column of Result except the two blobs, in struct-field order, qualified with the given table alias ("" for none).

It is derived from the model's own bun tags rather than transcribed, so a column added to Result reaches this projection automatically instead of silently scanning as a zero value. TestResultColumnsWithoutBlobs pins that.

func RollupPageStatus

func RollupPageStatus(resources []PageResourceStatus) (PageStatus, PageStatusCounts)

RollupPageStatus computes a status page's overall status from the live status of every resource across every section of the page. It is pure and exported — no DB access, no request context — so spec 2026-08-08-06 (the public summary endpoint) and spec 2026-08-08-07 (the page-level SVG badge) can call it directly against the same resource list ViewStatusPage builds, without reimplementing the aggregation.

Rules, evaluated in priority order over the whole resource list:

  1. Resources currently in maintenance are excluded from the outage scan — maintenance masks failures, so a resource that goes down *during* its own maintenance window never taints the page.
  2. Else any non-maintenance resource with CheckStatusDown -> PageStatusDown.
  3. Else any resource with CheckStatusDegraded or CheckStatusWarning -> PageStatusDegraded.
  4. Else if any resource is in maintenance -> PageStatusMaintenance.
  5. Else if no resource has a usable status (all CheckStatusCreated / no resources at all) -> PageStatusUnknown.
  6. Else -> PageStatusOperational.

func StateKey

func StateKey(parts ...string) string

StateKey joins parts with colons to create namespaced keys. Example: StateKey("incident", incidentUID, "slack_notification") returns "incident:abc123:slack_notification".

func Status

func Status(window *MaintenanceWindow, now time.Time) string

Status returns "active", "upcoming", or "past" for the window at now.

It is the canonical recurrence-status semantics shared with the frontend (computeMaintenanceStatus): active if IsActiveAt; else past for a non-recurring window after its start, or a recurring window after its RecurrenceEnd; else upcoming.

func StatusToString

func StatusToString(status int) string

StatusToString converts a ResultStatus integer to its string representation.

func TruncateSupportBody

func TruncateSupportBody(body string) (string, bool)

TruncateSupportBody caps a body at SupportBodyMaxLength, reporting whether it had to cut. Rune-aware, so a cap never splits a multi-byte character.

func ValidCustomDomainState

func ValidCustomDomainState(s string) bool

ValidCustomDomainState reports whether s is one of the lifecycle states.

func ValidStatusPageVisibility

func ValidStatusPageVisibility(v string) bool

ValidStatusPageVisibility reports whether v is one of the three supported visibility values.

func ValidSupportChannel

func ValidSupportChannel(channel string) bool

ValidSupportChannel reports whether a channel value is one we accept.

func ValidSupportStatus

func ValidSupportStatus(status string) bool

ValidSupportStatus reports whether a status value is one we accept.

func ValidateCapabilitySet

func ValidateCapabilitySet(capabilities []string) error

ValidateCapabilitySet reports whether a reported set is storable: every name a lowercase `[a-z0-9-]+` slug, and no duplicates. It is the Go mirror of the database CHECK constraint (Postgres) and triggers (SQLite).

IT EXISTS TO PROTECT LIVENESS, NOT TO REPLACE THE DATABASE. A capability set arrives from a remote agent, so it is untrusted input on a code path that also refreshes last_active_at. Letting a malformed array fail the whole UPDATE would take the region's liveness down with it — the agent would still be running checks while its region quietly went stale. Callers reject the set (falling back to "not reported", which changes nothing) and still write the heartbeat. The database remains the authority that garbage is never stored.

A nil set is valid: it means "not reported".

func VerifiableContactTypes

func VerifiableContactTypes() map[string]bool

VerifiableContactTypes are the contact types that require a code round-trip before they may be paged. Email and web push are self-verifying (delivery respectively subscription is the proof), so they are absent here.

Types

type ActorType

type ActorType string

ActorType represents who triggered an event.

const (
	// ActorTypeSystem indicates the event was triggered by the system.
	ActorTypeSystem ActorType = "system"
	// ActorTypeUser indicates the event was triggered by a user.
	ActorTypeUser ActorType = "user"
	// ActorTypeAPIToken indicates the event was triggered through a personal
	// access token or agent key rather than an interactive session. ActorUID
	// still names the owning user when one is known.
	ActorTypeAPIToken ActorType = "api_token"
	// ActorTypeService indicates the event was triggered by another trusted
	// service (a signed service-to-service call, e.g. the billing service).
	ActorTypeService ActorType = "service"
)

func (ActorType) IsValid

func (a ActorType) IsValid() bool

IsValid reports whether the actor type is one the schema accepts. The events.actor_type check constraint enumerates exactly these four values, so an unknown value must never reach an INSERT.

type Agent

type Agent struct {
	UID string `bun:"uid,pk,type:varchar(36)"`
	// OrganizationUID is nil exactly for system agents.
	OrganizationUID *string `bun:"organization_uid"`
	// Kind is AgentKindOrg or AgentKindSystem.
	Kind string `bun:"kind,notnull"`
	// Region is the region slug the agent is bound to; all its claims are
	// hard-scoped to it. Fully-qualified `@<org>/<region>` for an org agent, a
	// plain cloud region slug for a system agent.
	Region string `bun:"region,notnull"`
	Name   string `bun:"name,notnull"`
	// Ed25519PublicKey is the base64 identity public key used to verify reconnect
	// signatures.
	Ed25519PublicKey string `bun:"ed25519_public_key,notnull"`
	// X25519PublicKey is the age recipient string ("age1…") credentials are
	// sealed to.
	X25519PublicKey string `bun:"x25519_public_key,notnull"`
	// Fingerprint is a short hash of the Ed25519 key, shown in UI/logs.
	Fingerprint string     `bun:"fingerprint,notnull"`
	Status      string     `bun:"status,notnull"`
	LastSeenAt  *time.Time `bun:"last_seen_at"`
	EnrolledAt  time.Time  `bun:"enrolled_at,notnull,default:current_timestamp"`
	RevokedAt   *time.Time `bun:"revoked_at"`
	CreatedAt   time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt   time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt   *time.Time `bun:"deleted_at"`
}

Agent is a deported check agent that connects outbound-only over WebSocket, is hard-scoped to exactly one region, and can do nothing but claim/execute/submit checks. The DB never holds a usable agent credential — only the agent's public keys.

Kind decides the scope: an `org` agent is bound to one organization and one private region; a `system` agent has no organization at all and serves one shared cloud region for every org. The DB enforces the pairing (kind = 'org' <=> organization_uid is not null).

func NewAgent

func NewAgent(orgUID, region, name, ed25519Pub, x25519Pub, fingerprint string) *Agent

NewAgent builds an active org (tenant-private) agent row from an enrollment.

func NewSystemAgent

func NewSystemAgent(region, name, ed25519Pub, x25519Pub, fingerprint string) *Agent

NewSystemAgent builds an active platform-operated agent row. It carries no organization: its claims are scoped by the shared cloud region alone.

func (*Agent) IsSystem

func (a *Agent) IsSystem() bool

IsSystem reports whether this is a platform-operated agent (no owning org, shared cloud region).

func (*Agent) OrgUID

func (a *Agent) OrgUID() string

OrgUID returns the owning organization UID, or "" for a system agent.

type AgentEnrollmentToken

type AgentEnrollmentToken struct {
	UID string `bun:"uid,pk,type:varchar(36)"`
	// OrganizationUID is nil exactly for system tokens.
	OrganizationUID *string `bun:"organization_uid"`
	// Kind is AgentKindOrg or AgentKindSystem.
	Kind string `bun:"kind,notnull"`
	// Region is the region the enrolled agent will be bound to:
	// `@<org>/<region>` for an org token, a cloud region slug for a system one.
	Region string `bun:"region,notnull"`
	// TokenHash is the SHA-256 hex of the spe_ token — never the token itself.
	TokenHash string    `bun:"token_hash,notnull"`
	ExpiresAt time.Time `bun:"expires_at,notnull"`
	// MaxUses bounds a system token's enrollments (nil = unlimited). Never set
	// on an org token, which is one-shot by construction.
	MaxUses *int `bun:"max_uses"`
	// UseCount is how many agents enrolled with this token (0 or 1 for org).
	UseCount int `bun:"use_count,notnull"`
	// UsedAt / UsedByAgentUID record the LAST enrollment. For an org token they
	// are also the single-use marker.
	UsedAt           *time.Time `bun:"used_at"`
	UsedByAgentUID   *string    `bun:"used_by_agent_uid"`
	CreatedByUserUID *string    `bun:"created_by_user_uid"`
	CreatedAt        time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	DeletedAt        *time.Time `bun:"deleted_at"`
}

AgentEnrollmentToken binds a future agent to a region. Only the SHA-256 hash of the token is stored; the token itself is readable exactly once.

An `org` token is minted by an org admin, bound to (org, private region), and strictly ONE-SHOT: enrollment atomically marks it used while creating the agent row. A `system` token is platform-operator material seeded from SP_SYSTEM_AGENT_ENROLLMENT_TOKENS, bound to a cloud region slug and to no org, and MULTI-USE (bounded by MaxUses, nil = unlimited): every machine of a fly fleet generates its own keypair and enrolls on boot, so no private key is ever shared between machines.

func NewAgentEnrollmentToken

func NewAgentEnrollmentToken(
	orgUID, region, tokenHash string, expiresAt time.Time, createdByUserUID *string,
) *AgentEnrollmentToken

NewAgentEnrollmentToken builds a one-shot org enrollment token row bound to (org, private region) with the given hash and expiry.

func NewSystemAgentEnrollmentToken

func NewSystemAgentEnrollmentToken(
	region, tokenHash string, expiresAt time.Time, maxUses *int,
) *AgentEnrollmentToken

NewSystemAgentEnrollmentToken builds a multi-use platform enrollment token bound to a cloud region slug (no owning org). maxUses nil = unlimited.

func (*AgentEnrollmentToken) HasUsesLeft

func (t *AgentEnrollmentToken) HasUsesLeft() bool

HasUsesLeft reports whether the token may still enroll an agent. Org tokens are one-shot (the atomic consume is the real guard); system tokens are unlimited unless MaxUses is set.

func (*AgentEnrollmentToken) IsSystem

func (t *AgentEnrollmentToken) IsSystem() bool

IsSystem reports whether this is a platform (multi-use) enrollment token.

func (*AgentEnrollmentToken) OrgUID

func (t *AgentEnrollmentToken) OrgUID() string

OrgUID returns the owning organization UID, or "" for a system token.

type AgentNonce

type AgentNonce struct {
	AgentUID string    `bun:"agent_uid,pk"`
	Nonce    string    `bun:"nonce,pk"`
	SeenAt   time.Time `bun:"seen_at,notnull"`
}

AgentNonce is one consumed reconnect nonce. The table is the cluster-wide replay guard: the per-process cache it replaces was only sound with a single API replica, which a multi-machine fly fleet reconnecting through a load balancer is not.

type AggregateResultsFunc

type AggregateResultsFunc func(sources []*Result) (rollup *Result, sourceUIDs []string, err error)

AggregateResultsFunc computes the single rollup row for one bucket from its source rows and returns the UIDs of the source rows to delete. It runs inside CompactResults' transaction and must be pure (no DB access). Returning a nil rollup or an empty sourceUIDs signals "nothing measurable to compact" — the sources are left in place and the transaction commits without writing.

type AppSetting

type AppSetting struct {
	Key       string    `bun:"key,pk,type:text"`
	Value     string    `bun:"value,notnull,type:text"`
	UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

AppSetting is a generic key/value store for server-level configuration that must survive restarts (e.g., VAPID keys for Web Push).

type AutoResolvePolicy

type AutoResolvePolicy string

AutoResolvePolicy decides what an auto-created publication does when its backing incident resolves.

const (
	// AutoResolveAlways resolves the publication regardless of human edits.
	AutoResolveAlways AutoResolvePolicy = "always"
	// AutoResolveIfUntouched (default) resolves only while nobody has edited
	// the publication; once a human owns the narrative, the automation posts a
	// "component recovered" monitoring note and leaves the final resolve to
	// them. It applies to every publication LINKED to the resolving incident,
	// hand-published ones included — only free-form entries are out of scope.
	AutoResolveIfUntouched AutoResolvePolicy = "if_untouched"
	// AutoResolveNever posts nothing and resolves nothing.
	AutoResolveNever AutoResolvePolicy = "never"
)

AutoResolvePolicy values.

func (AutoResolvePolicy) IsValid

func (p AutoResolvePolicy) IsValid() bool

IsValid reports whether the policy is one of the three recognized values.

type AvailabilityCounts

type AvailabilityCounts struct {
	Success int64
	Total   int64
}

AvailabilityCounts is a (success, countable-total) tally over some window, potentially folded together from more than one period_type tier (e.g. an org's trailing-24h availability combines `hour` rollups and `raw` rows — see checks.Service.GetCheckStats / spec 2026-08-26-09). Callers derive the percentage via Pct rather than dividing directly, so "no measurable data" (Total == 0) can never be silently rendered as a manufactured 100%.

func (AvailabilityCounts) Pct

func (c AvailabilityCounts) Pct() (float64, bool)

Pct returns 100*Success/Total and ok=true when Total > 0. ok=false means the window had no countable data at all — the caller must render that as "no data", never as a fabricated percentage.

type AvailabilitySettings

type AvailabilitySettings struct {
	// ThresholdUp is the green floor (pct >= this renders "up"). nil = 99.9.
	ThresholdUp *float64 `json:"thresholdUp,omitempty"`
	// ThresholdDegraded is the amber floor (pct >= this and < ThresholdUp
	// renders "degraded"; below it renders "down", subject to the
	// small-bucket calibration guard). nil = 99.0.
	ThresholdDegraded *float64 `json:"thresholdDegraded,omitempty"`
}

AvailabilitySettings customizes the green/amber/red availability thresholds rendered on a status page's bars. Nil fields fall back to the package defaults (DefaultAvailabilityThresholdUp/Degraded).

func (*AvailabilitySettings) EffectiveThresholds

func (a *AvailabilitySettings) EffectiveThresholds() (float64, float64)

EffectiveThresholds resolves the up/degraded thresholds for a (possibly nil) AvailabilitySettings, falling back to the package defaults for a nil receiver or nil fields. Never returns a value that needs further nil checking by the caller.

type BrandingSettings

type BrandingSettings struct {
	// LogoFileUID is the `files.uid` of the page's own logo. nil = wear the
	// SolidPing logo.
	LogoFileUID *string `json:"logoFileUid,omitempty"`
	// FaviconFileUID is the `files.uid` of the page's own favicon. nil = the
	// default favicon.
	FaviconFileUID *string `json:"faviconFileUid,omitempty"`
	// HideBranding is the page's HALF of the white-label decision: the
	// "powered by SolidPing" footer disappears only when this is true AND the
	// org holds the `whiteLabel` entitlement. Keeping the two halves separate
	// means a downgrade silently restores the badge without rewriting the page.
	HideBranding bool `json:"hideBranding,omitempty"`
}

BrandingSettings is the page's brand identity. File UIDs, not URLs — the public URL is derived (statuspageassets.PublicURL), so the stored value stays valid when the route changes.

type Capabilities

type Capabilities struct {
	// CanNotify reports whether the integration can receive outbound
	// notifications (i.e. it can act as a "channel" / notification target).
	CanNotify bool
	// CanSource reports whether the integration provides data that checks
	// read from (e.g. the Freebox line-quality source).
	CanSource bool
	// CanSendSMS / CanPlaceCall report the two PHONE capabilities, resolved
	// independently of each other. They are separate because the providers
	// are: OVHcloud sells SMS but has no voice API, so an instance can run OVH
	// for SMS and Twilio for voice at the same time. Binding them would mean
	// giving up escalation calls in exchange for cheaper SMS.
	CanSendSMS   bool
	CanPlaceCall bool
}

Capabilities describes what roles an integration type can play. The two flags are independent: a single type may be both a notification sink and a data source. They replace the former hard-coded "Freebox is a source, not a sink" carve-out in notifications.GetSender — capability is now data, not a special case.

func CapabilitiesFor

func CapabilitiesFor(t ConnectionType) Capabilities

CapabilitiesFor returns the capabilities of an integration connection type. Every notification sink (slack, discord, webhook, email, googlechat, mattermost, msteams, ntfy, gotify, zulip, pagerduty, pushover) is CanNotify; freebox is a data source (CanSource) and cannot receive notifications. Twilio additionally carries the two phone capabilities — it is the only connection type that is a bring-your-own SMS *and* voice account. The default branch intentionally covers every current notification-sink type, so only data sources and the phone type need an explicit case.

type CapabilityState

type CapabilityState uint8

CapabilityState is the THREE-state answer to "does this worker have X?". A distinct type rather than a bool, so a caller physically cannot collapse "unknown" into "no".

const (
	// CapabilityStateUnknown means the worker has never reported its set.
	CapabilityStateUnknown CapabilityState = iota
	// CapabilityStateAbsent means the worker reported, and this capability was
	// not in the set. That is a real "no", not an absence of information.
	CapabilityStateAbsent
	// CapabilityStatePresent means the worker reported this capability.
	CapabilityStatePresent
)

type Check

type Check struct {
	UID             string  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string  `bun:"organization_uid,notnull"`
	CheckGroupUID   *string `bun:"check_group_uid"`
	Name            *string `bun:"name"`
	Slug            *string `bun:"slug"`
	Description     *string `bun:"description"`
	Type            string  `bun:"type,notnull"`
	Config          JSONMap `bun:"config,type:jsonb,nullzero"`
	// ConfigPrivate holds the AES-GCM envelope (JSON) for the secret keys
	// split out of Config at write time. NULL when no encrypted secrets exist
	// on this row — distinct from "encryption disabled at the server".
	ConfigPrivate *string `bun:"config_private,type:text,nullzero"`
	// ConfigPrivateKeys is a JSON array of the key names (e.g. `["password"]`)
	// whose values live in ConfigPrivate. Non-secret by construction; surfaced
	// to the dashboard so it can render placeholder hints without decrypting.
	ConfigPrivateKeys *string `bun:"config_private_keys,type:text,nullzero"`
	// ConfigSealed holds the region-sealed (age X25519, v2) envelope of the same
	// secret keys when the check targets one or more org-private regions (spec
	// 2026-07-16-02): sealed to the X25519 keys of the region's active agents.
	// A check targeting ONLY private regions stores secrets sealed-only
	// (ConfigPrivate stays NULL — the server cannot decrypt them after write);
	// a mixed private+cloud check dual-stores (v1 envelope for cloud dispatch +
	// this sealed blob for agents).
	ConfigSealed *string            `bun:"config_sealed,type:text,nullzero"`
	Regions      []string           `bun:"regions,type:text[],array"`
	Enabled      bool               `bun:"enabled,notnull"`
	Internal     bool               `bun:"internal,notnull"`
	Period       timeutils.Duration `bun:"period,notnull"`

	// CreatedBy is the users.uid of whoever created this check, or NULL when
	// nobody did — the startup job's seeded samples, and every check that
	// predates the column (spec 2026-09-06-02). It is recorded for EVERY
	// creator, not only demo sessions: "who made this" is useful audit data in
	// its own right, and a column populated on one code path only is a column
	// nobody can trust.
	//
	// Deliberately not a foreign key: a check outlives the account that made
	// it, and users are soft-deleted. This is a historical attribution.
	//
	// It is also what makes seeded demo checks immutable to a demo session
	// without any "protected" flag: the ownership rule is
	// `created_by == claims.UserUID`, and NULL never equals a UID.
	CreatedBy *string `bun:"created_by,nullzero"`

	// RegionSpread is the optional inter-region scheduling offset ("spread")
	// applied between consecutive regions' phases (spec 2026-07-20-05). NULL =
	// the default of Period / region_count (even coverage across the period);
	// a non-null value forces a fixed offset (e.g. 0 = all regions fire
	// together for comparative cross-region sampling), validated
	// 0 <= RegionSpread < Period. It is a first-class scheduling input (it
	// drives check_jobs phase), not checker config, hence a column like Period.
	RegionSpread *timeutils.Duration `bun:"region_spread,nullzero"`

	// Incident tracking — wall-clock periods (seconds). Replaces the old
	// count-based thresholds per spec
	// 2026-05-08-02-time-based-confirmation-and-recovery-periods.md.
	// `0` means "open / resolve immediately on the first opposite signal".
	ConfirmationPeriodSeconds int `bun:"confirmation_period_seconds,notnull"`
	RecoveryPeriodSeconds     int `bun:"recovery_period_seconds,notnull"`
	// EscalationThreshold remains streak-based for now — it gates the *second*
	// notification step, not the incident open. Will be re-modeled when the
	// escalation-severity primitive ships. No `default:` clause even though
	// the column has one — see the StatusPage.AutoPublishDelaySeconds note:
	// `default:3` made `escalation_threshold: 0` unwritable on create.
	EscalationThreshold int `bun:"escalation_threshold,notnull"`
	// FirstFailureAt is set on the result that flips the streak from 0 to 1
	// on a failing check (no active incident yet). Cleared on the next success.
	// The incident opens when now - FirstFailureAt >= ConfirmationPeriod.
	FirstFailureAt *time.Time `bun:"first_failure_at"`
	// FirstSuccessSinceFailureAt is set on the first success arriving while
	// an incident is open. Cleared by any subsequent failure during the
	// recovery window. Auto-resolve fires when
	// now - FirstSuccessSinceFailureAt >= RecoveryPeriod.
	FirstSuccessSinceFailureAt *time.Time `bun:"first_success_since_failure_at"`

	// Adaptive resolution settings.
	//
	// ReopenCooldownMultiplier (nil = code default) drives the short
	// blip-dedup window: a fast relapse reattaches to the just-resolved
	// incident instead of paging again. Independent of the flapping layer.
	ReopenCooldownMultiplier *int `bun:"reopen_cooldown_multiplier"`

	// Flapping (adaptive recovery) config — spec 2026-06-30-07. When a check
	// flaps (repeated outages over a short horizon) the required stability
	// before auto-resolving grows per flap, bounded by a cap. Off-by-default-
	// equivalent: FlapBackoffFactor==1 or FlappingWindowSeconds==0 reproduces
	// the constant RecoveryPeriodSeconds behavior.
	//
	// Which is precisely why none of the three carries a `default:` clause,
	// even though all three columns have one — see the
	// StatusPage.AutoPublishDelaySeconds note. With `default:21600` on the tag,
	// `flappingWindowSeconds: 0` never reached the database, so flapping could
	// not be turned off at creation time (spec 2026-08-30-04). NewCheck
	// supplies the 21600/2/8 defaults instead.
	FlappingWindowSeconds int `bun:"flapping_window_seconds,notnull"`
	FlapBackoffFactor     int `bun:"flap_backoff_factor,notnull"`
	MaxRecoveryMultiplier int `bun:"max_recovery_multiplier,notnull"`

	// Flap state, updated only on the rare incident-open/reopen (never per
	// result). FlapCount is the number of outages accumulated inside the
	// rolling flapping window; LastOutageAt is the wall-clock of the most
	// recent outage onset and gates the window reset.
	FlapCount    int        `bun:"flap_count,notnull"`
	LastOutageAt *time.Time `bun:"last_outage_at"`

	// Optional escalation policy. Falls back to the check_group's policy
	// (and ultimately to no escalation) when nil.
	EscalationPolicyUID *string `bun:"escalation_policy_uid"`

	// TracerouteOnFailure is the per-check override for the MTR-style path
	// capture taken when this check goes down on a network-reachability
	// failure (spec 2026-08-21-10).
	//
	// THREE STATES, AND nil IS THE INTERESTING ONE:
	//
	//	nil     inherit the org default (org parameter
	//	        `diagnostics.traceroute.enabled`, itself ON per the spec)
	//	&true   always trace this check
	//	&false  never trace this check, whatever the org default says
	//
	// A plain bool would collapse "not decided" into "no", which would make
	// the org-level default unreachable for every check that already exists.
	TracerouteOnFailure *bool `bun:"traceroute_on_failure"`

	// Status tracking
	Status          CheckStatus `bun:"status,notnull"`
	StatusStreak    int         `bun:"status_streak,notnull"`
	StatusChangedAt *time.Time  `bun:"status_changed_at"`

	CreatedAt time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt *time.Time `bun:"deleted_at"`

	// GroupSortKey is the effective group-ordering key populated only by the
	// sort=group ListChecks path: the check's group sort_order, or a large
	// sentinel (int16 max is 32767, so ungrouped sorts strictly last). Scan-only
	// and transient — never selected, inserted, or updated outside that query.
	GroupSortKey int64 `bun:"group_sort_key,scanonly"`

	// TargetHostSortKey is the effective sort=targetHost ordering key: the
	// check's config host/url/target text (best-effort, not hostname-parsed —
	// see targetHostSortKeyExpr), or a sentinel that sorts strictly last for
	// checks with none of those fields. Scan-only and transient; distinct from
	// the response's TargetHost (checkerdef.ExtractTargetHost), which is the
	// precise, hostname-parsed value clients bucket by.
	TargetHostSortKey string `bun:"target_host_sort_key,scanonly"`
}

Check represents a monitoring configuration.

func NewCheck

func NewCheck(orgUID, slug, checkType string) *Check

NewCheck creates a new check with generated UID.

func (*Check) EffectiveFlapCount

func (c *Check) EffectiveFlapCount(now time.Time) int

EffectiveFlapCount returns the number of outages counted inside the current rolling flapping window, as of `now`.

THE LAZY-RESET TRAP: FlapCount (the raw column) only resets to 0 at the NEXT outage onset (see incidents.bumpFlap) — a check whose last outage was e.g. 12h ago can still hold a stale nonzero FlapCount in the row, because nothing has come along yet to reset it. Any caller that reads FlapCount directly to describe the check's CURRENT state (rather than to drive the active incident's own recovery math, where it is always fresh) must use this method instead, or it will report a flap level that stopped being true hours or days ago.

func (*Check) EffectiveRecoveryPeriod

func (c *Check) EffectiveRecoveryPeriod() time.Duration

EffectiveRecoveryPeriod returns the stability required before an incident on this check auto-resolves, given the check's RAW (possibly stale) FlapCount. This is what the incidents package uses while an incident is active: FlapCount was just written by bumpFlap at this incident's own onset, so it is always fresh in that context — the lazy-reset trap does not apply here. See effectiveRecoveryPeriodForFlapCount for the math.

func (*Check) EffectiveRecoveryPeriodAt

func (c *Check) EffectiveRecoveryPeriodAt(now time.Time) time.Duration

EffectiveRecoveryPeriodAt returns the same computation as EffectiveRecoveryPeriod, but driven by EffectiveFlapCount(now) rather than the raw column — i.e. it is lazy-reset aware. Use this to describe a check's CURRENT adaptive-recovery state from outside an active incident (e.g. the API's flapState block), where the raw FlapCount may be stale.

func (*Check) FlappingWindowElapsed

func (c *Check) FlappingWindowElapsed(now time.Time) bool

FlappingWindowElapsed reports whether the rolling flapping window has elapsed as of `now`, i.e. whether the NEXT outage onset would start a fresh window rather than count as a flap inside the current one. True when there has been no outage yet (LastOutageAt nil), the flapping feature is off (FlappingWindowSeconds == 0), or the last outage is older than the window.

This is the one place the "lazy reset" rule is expressed — both the write-path counter bump (incidents.bumpFlap, via this method) and the read-path effective-value exposure (EffectiveFlapCount) delegate to it, so the two can never drift apart.

func (*Check) RegionSpreadDuration

func (c *Check) RegionSpreadDuration() *time.Duration

RegionSpreadDuration returns the check's optional inter-region spread override as a *time.Duration (nil when unset), for the scheduling.RegionSpread resolver. Keeps the *timeutils.Duration ⇄ *time.Duration conversion in one place so the reconcile, create, and worker paths all resolve the identical spread.

func (*Check) TimeoutOrDefault

func (c *Check) TimeoutOrDefault(defaultTimeout time.Duration) time.Duration

TimeoutOrDefault resolves the check's per-execution timeout: the explicit `timeout` entry in its config when it parses to a positive duration, and `defaultTimeout` (the server's scheduling.check_timeout_ms) otherwise.

This is a READ-side approximation of what the worker actually applies — the worker additionally clamps an unset timeout by the cost EWMA and caps an explicit one (checkworker.perCheckTimeout). Both consumers here want an upper bound on "how late can this check possibly notice an outage", and the unclamped default is exactly that bound: the cost-aware clamp only ever shortens it.

type CheckConnection

type CheckConnection struct {
	bun.BaseModel `bun:"table:check_channels,alias:check_channel"`

	UID             string    `bun:"uid,pk,type:varchar(36)"`
	CheckUID        string    `bun:"check_uid,notnull"`
	ConnectionUID   string    `bun:"integration_uid,notnull"`
	OrganizationUID string    `bun:"organization_uid,notnull"`
	Settings        *JSONMap  `bun:"settings,type:jsonb"`
	CreatedAt       time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time `bun:"updated_at,notnull,default:current_timestamp"`

	// Relations (optional, for eager loading)
	Check        *Check        `bun:"rel:belongs-to,join:check_uid=uid"`
	Connection   *Integration  `bun:"rel:belongs-to,join:integration_uid=uid"`
	Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}

CheckConnection represents the many-to-many relationship between checks and the integrations they notify through. Backed by the `check_channels` table (the binding keeps the "channel" name to match the notify-role taxonomy); the FK column is `integration_uid`. The Go field name ConnectionUID is retained.

func NewCheckConnection

func NewCheckConnection(checkUID, connectionUID, organizationUID string) *CheckConnection

NewCheckConnection creates a new check-connection relationship with generated UID.

type CheckConnectionUpdate

type CheckConnectionUpdate struct {
	Settings *JSONMap
}

CheckConnectionUpdate represents fields that can be updated for a check-connection.

type CheckDependency

type CheckDependency struct {
	UID             string              `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string              `bun:"organization_uid,notnull"`
	ParentCheckUID  string              `bun:"parent_check_uid,notnull"`
	ChildCheckUID   string              `bun:"child_check_uid,notnull"`
	Kind            CheckDependencyKind `bun:"kind,notnull"`
	Description     *string             `bun:"description"`
	CreatedAt       time.Time           `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time           `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time          `bun:"deleted_at"`
}

CheckDependency is a directed edge (parent → child) inside an org's dependency DAG. The same org constraint is enforced at write time.

func NewCheckDependency

func NewCheckDependency(
	orgUID, parentUID, childUID string,
	kind CheckDependencyKind,
	description *string,
) *CheckDependency

NewCheckDependency builds a fresh edge.

type CheckDependencyKind

type CheckDependencyKind string

CheckDependencyKind enumerates whether a parent failure is a hard or soft cause for the child. Hard edges drive rollup (paging suppression); soft edges are informational only.

const (
	// CheckDependencyKindHard means the child reliably fails when the parent does.
	CheckDependencyKindHard CheckDependencyKind = "hard"
	// CheckDependencyKindSoft means the child may degrade but won't necessarily fail.
	CheckDependencyKindSoft CheckDependencyKind = "soft"
)

func (CheckDependencyKind) IsValid

func (k CheckDependencyKind) IsValid() bool

IsValid reports whether the value is one of the known kinds.

type CheckDependencyUpdate

type CheckDependencyUpdate struct {
	Kind             *CheckDependencyKind
	Description      *string
	ClearDescription bool
}

CheckDependencyUpdate captures the writable fields. Pointer = optional.

type CheckGroup

type CheckGroup struct {
	UID             string  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string  `bun:"organization_uid,notnull"`
	Name            string  `bun:"name,notnull"`
	Slug            string  `bun:"slug,notnull"`
	Description     *string `bun:"description"`
	SortOrder       int16   `bun:"sort_order,notnull"`
	// Optional escalation policy. NULL = no group-level policy.
	EscalationPolicyUID *string    `bun:"escalation_policy_uid"`
	CreatedAt           time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt           time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt           *time.Time `bun:"deleted_at"`

	// Computed field (not stored in DB)
	CheckCount int `bun:"check_count,scanonly"`
}

CheckGroup represents a flat organizational group for checks.

func NewCheckGroup

func NewCheckGroup(orgUID, name, slug string) *CheckGroup

NewCheckGroup creates a new check group with generated UID.

type CheckGroupUpdate

type CheckGroupUpdate struct {
	Name                *string
	Slug                *string
	Description         *string
	SortOrder           *int16
	EscalationPolicyUID *string

	ClearEscalationPolicyUID bool
}

CheckGroupUpdate represents fields that can be updated on a check group.

type CheckJob

type CheckJob struct {
	UID             string  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string  `bun:"organization_uid,notnull"`
	CheckUID        string  `bun:"check_uid"`
	Region          *string `bun:"region"`
	Type            string  `bun:"type"`
	Config          JSONMap `bun:"config,type:jsonb,nullzero"`
	// ConfigPrivate / ConfigPrivateKeys mirror the columns on Check — the
	// scheduler copies them when materializing a job so workers never see
	// plaintext secrets in the row at rest.
	ConfigPrivate     *string `bun:"config_private,type:text,nullzero"`
	ConfigPrivateKeys *string `bun:"config_private_keys,type:text,nullzero"`
	// ConfigSealed mirrors Check.ConfigSealed: the region-sealed (age X25519)
	// envelope shipped VERBATIM to deported agents — the server never decrypts
	// it on the agent dispatch path (spec 2026-07-16-02).
	ConfigSealed   *string            `bun:"config_sealed,type:text,nullzero"`
	Encrypted      bool               `bun:"encrypted,notnull"`
	Period         timeutils.Duration `bun:"period,notnull"`
	ScheduledAt    *time.Time         `bun:"scheduled_at"`
	LeaseWorkerUID *string            `bun:"lease_worker_uid"`
	LeaseExpiresAt *time.Time         `bun:"lease_expires_at"`
	LeaseStarts    int                `bun:"lease_starts,notnull"`
	UpdatedAt      time.Time          `bun:"updated_at,notnull,default:current_timestamp"`

	// Cost-aware, plan-weighted scheduling (spec 2026-06-30-09).
	//
	// CostEWMAMs is an exponentially-weighted moving average of execution
	// duration in milliseconds, updated in the post-exec write (ReleaseLease).
	// Timeouts pin it to the ceiling. Drives slow-lane classification and the
	// cost-aware execution timeout. 0 until the job's first run.
	CostEWMAMs float64 `bun:"cost_ewma_ms,notnull"`
	// DelayEWMAMs is an EWMA of how late the job actually started relative to
	// its real scheduled_at (probe start − scheduled_at, floored at 0), updated
	// in the post-exec write. Pure telemetry (spec 2026-07-01-02): it feeds the
	// cost-distribution endpoint and the lane-split go/no-go but never the claim
	// order — delay is a victim signal, and folding it into the offset punished
	// starved checks and spiraled unboundedly under overload. 0 until the job's
	// first run. Added by migration 007.
	DelayEWMAMs float64 `bun:"delay_ewma_ms,notnull"`
	// PlanWeight is the denormalized plan tier copied from org_entitlements
	// (0 = free; higher = more protected). Reserved capacity + deadline credit
	// for paid orgs. Refreshed on entitlement change and reconcile.
	PlanWeight int `bun:"plan_weight,notnull"`
	// EffectiveScheduledAt is scheduled_at + cost_ewma×CostOffsetWeight −
	// tier_credit, with the offset clamped to scheduling.MaxDeprioritizeOffset.
	// The claim SELECT gates on scheduled_at but orders by this column, so
	// de-prioritization only bites under contention (D2/Option A). Backfilled to
	// scheduled_at by migration 006; delay-era offsets healed by migration 008.
	//
	// A rate-limited deferral deliberately does NOT re-anchor this column
	// (checkjobsvc.DeferLeaseRateLimited, spec 2026-08-26-02): a job the per-org
	// bucket turned away keeps the tick it missed as its ordering key, so it
	// grows more overdue every window it loses and wins the next contended slot.
	// That is what makes an over-cap org rotate its deficit instead of starving
	// the same UID-hash phases forever.
	EffectiveScheduledAt *time.Time `bun:"effective_scheduled_at"`
	// Lane is the scheduling class (spec 2026-07-01-03): 0 = fast, 1 = slow
	// (scheduling.LaneFast / LaneSlow). Classified from the cost EWMA with
	// hysteresis in the post-exec write; new rows start fast (first run is
	// FIFO; one execution reclassifies). The claim runs two lane-filtered
	// SELECTs so slow jobs can never occupy more than pool_size −
	// fast_lane_reserved slots on a worker. Added by migration 009.
	Lane uint8 `bun:"lane,notnull"`

	// Check is the check this job executes, populated at claim time by
	// ClaimJobs / ClaimJobsForCheck so the incident hot path can skip a
	// per-result GetCheck round-trip. Transient: never persisted (bun:"-").
	// May be nil if the check was deleted between scheduling and claim.
	Check *Check `bun:"-"`
}

CheckJob represents a scheduled job for executing a check.

func NewCheckJob

func NewCheckJob(orgUID string, checkUID string, period timeutils.Duration) *CheckJob

NewCheckJob creates a new check job with generated UID.

func (*CheckJob) IsInternal

func (j *CheckJob) IsInternal() bool

IsInternal reports whether this job belongs to an internal, server-created check (worker self-stats plumbing).

ONE HELPER SO THE TWO RATE GATES CANNOT DISAGREE — the in-process worker gate (checkworker.applyRateLimitGate) and the agent dispatch gate (agentws.handleClaim) both call it, the same way both delegate "what is a passive check" to checkerdef. An internal check is exempt from the MaxChecks quota and invisible to the checks-per-minute demand figure, so it must not draw MaxChecksPerMinute tokens either (spec 2026-08-27-01): counting nowhere has to mean counting nowhere, or the predicted demand and the factual skip counter describe different fleets.

Reads the check attached at claim time (checkjobsvc.attachChecks, inside the claim transaction) — no column on check_jobs, no migration. A nil Check means the row was deleted between scheduling and claim; that job is treated as a normal metered one, which is the safe direction.

type CheckJobUpdate

type CheckJobUpdate struct {
	Region            *string
	Config            *JSONMap
	ConfigPrivate     *string
	ConfigPrivateKeys *string
	Encrypted         *bool
	Period            *timeutils.Duration
	ScheduledAt       *time.Time
	LeaseWorkerUID    *string
	LeaseExpiresAt    *time.Time
	LeaseStarts       *int
}

CheckJobUpdate represents fields that can be updated.

type CheckLabel

type CheckLabel struct {
	UID       string    `bun:"uid,pk,type:varchar(36)"`
	CheckUID  string    `bun:"check_uid,notnull"`
	LabelUID  string    `bun:"label_uid,notnull"`
	CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}

CheckLabel represents the many-to-many relationship between checks and labels.

func NewCheckLabel

func NewCheckLabel(checkUID, labelUID string) *CheckLabel

NewCheckLabel creates a new check-label relationship with generated UID.

type CheckRate

type CheckRate struct {
	// UID identifies the row so a caller can project a hypothetical change:
	// drop the check being edited out of the sum and add its proposed shape
	// back (spec 2026-08-26-05's validate-time rate warning).
	UID     string             `bun:"uid"`
	Enabled bool               `bun:"enabled"`
	Period  timeutils.Duration `bun:"period"`
	Regions []string           `bun:"regions,type:text[],array"`
	Type    string             `bun:"type"`
}

CheckRate is a thin projection of a check used to compute usage stats: whether the check is enabled, its execution period, and its region set. Returned by ListOrgCheckRates so the entitlements service can sum the aggregate checks-per-minute in Go (the SQL interval/text representation of Period is not portable for a SUM(60/period) across Postgres and SQLite). Regions is needed because a multi-region check executes once per region per period, so its per-minute cost is (60s/period) × max(1, len(Regions)). Type is needed to exclude passive types (heartbeat, email) from the demand measured against MaxChecksPerMinute: they return before the token gate and consume no execution budget (spec 2026-08-26-03).

type CheckStatus

type CheckStatus int

CheckStatus represents the health status of a check.

const (
	// CheckStatusCreated indicates the check was just created and hasn't been executed yet.
	CheckStatusCreated CheckStatus = 1
	// CheckStatusUp indicates the check is healthy.
	CheckStatusUp CheckStatus = 3
	// CheckStatusDown indicates the check is failing.
	CheckStatusDown CheckStatus = 4
	// CheckStatusValidating is the transient state between "first failure
	// observed" and "incident opens" — the failure has been seen but the
	// configured ConfirmationPeriod hasn't elapsed yet. Display-only:
	// never triggers notifications, never gates the incident state machine.
	CheckStatusValidating CheckStatus = 5
	// CheckStatusDegraded is the aggregated/summary status: a rolled-up window
	// contained warning(s) but no dominating failure. Not produced by the live
	// pipeline (which uses CheckStatusWarning); retained for rendering a
	// check's aggregated/summary status and as a valid ?status= filter value.
	CheckStatusDegraded CheckStatus = 7
	// CheckStatusWarning is the live current status: the target is up but
	// there is something to report. Display-only like CheckStatusValidating —
	// never triggers notifications, never gates the incident state machine.
	CheckStatusWarning CheckStatus = 8
)

func RollupGroupStatus

func RollupGroupStatus(counts map[CheckStatus]int) CheckStatus

RollupGroupStatus derives a single, read-time status for a check group from the per-status counts of its considered member checks (spec 2026-08-01-01). Callers must pre-filter counts to enabled, non-deleted members — this function has no opinion on which checks are "in" the group, only on how to combine their statuses.

Deliberately Better-Stack-shaped: partial failure reads as "degraded" (not a blanket "down"), and a warning-only member doesn't paint the whole group red. Rules, in priority order:

  1. No considered members (or the map only contains CheckStatusCreated entries) → CheckStatusCreated.
  2. All considered members down → CheckStatusDown.
  3. Some but not all down → CheckStatusDegraded.
  4. No down, at least one warning → CheckStatusWarning.
  5. No down/warning, at least one validating → CheckStatusValidating.
  6. Otherwise, at least one up → CheckStatusUp.
  7. Falls back to CheckStatusCreated (e.g. members whose status doesn't match any of the above — in practice only CheckStatusCreated, since CheckStatusDegraded is never a live per-check status).

func (CheckStatus) String

func (s CheckStatus) String() string

String returns the lowercase wire name for a CheckStatus, used by the dashboard to key status colors and labels. Unknown values fall back to "unknown" so an unset DB column never blows up the UI.

type CheckStatusCount

type CheckStatusCount struct {
	Status  CheckStatus `bun:"status"`
	Enabled bool        `bun:"enabled"`
	Count   int         `bun:"count"`
}

CheckStatusCount is one row of the org-wide check aggregation (spec 2026-08-02-06): the number of checks sharing a (status, enabled) pair. Produced by db.Service.GetCheckStatusCounts on both dialects and folded into the checks stats response.

type CheckUpdate

type CheckUpdate struct {
	CheckGroupUID      *string
	Name               *string
	Slug               *string
	Description        *string
	Type               *string
	Config             *JSONMap
	ConfigPrivate      *string
	ConfigPrivateKeys  *string
	ClearConfigPrivate bool
	ConfigSealed       *string
	ClearConfigSealed  bool
	Regions            *[]string
	Enabled            *bool
	Internal           *bool
	Period             *timeutils.Duration
	// RegionSpread sets the inter-region offset override; ClearRegionSpread
	// resets it to NULL (revert to the period/region_count default).
	RegionSpread      *timeutils.Duration
	ClearRegionSpread bool

	// Incident tracking — wall-clock periods replacing the legacy count
	// thresholds. EscalationThreshold stays count-based for now.
	ConfirmationPeriodSeconds *int
	RecoveryPeriodSeconds     *int
	EscalationThreshold       *int

	// FirstFailureAt / FirstSuccessSinceFailureAt drive the open/resolve
	// clocks; ProcessCheckResult sets/clears them as the streak signal flips.
	FirstFailureAt                  *time.Time
	FirstSuccessSinceFailureAt      *time.Time
	ClearFirstFailureAt             bool
	ClearFirstSuccessSinceFailureAt bool

	// Adaptive resolution settings
	ReopenCooldownMultiplier *int

	// Flapping (adaptive recovery) config — spec 2026-06-30-07.
	FlappingWindowSeconds *int
	FlapBackoffFactor     *int
	MaxRecoveryMultiplier *int

	// Optional escalation policy override (nil = inherit from group / none)
	EscalationPolicyUID *string

	// TracerouteOnFailure sets the per-check path-trace override;
	// ClearTracerouteOnFailure resets it to NULL (inherit the org default).
	TracerouteOnFailure      *bool
	ClearTracerouteOnFailure bool

	// Clear* fields set the corresponding column to NULL on update.
	ClearEscalationPolicyUID bool

	// Status tracking (internal use)
	Status          *CheckStatus
	StatusStreak    *int
	StatusChangedAt *time.Time
}

CheckUpdate represents fields that can be updated.

type CompactResultsOutcome

type CompactResultsOutcome struct {
	// Fetched is the number of source rows read for the bucket.
	Fetched int
	// SourceCount is the number of measurable source rows the aggregate function
	// selected for deletion (its returned sourceUIDs). Zero for a marker-only
	// bucket.
	SourceCount int
	// Compacted is true when the rollup row was upserted and the source rows
	// deleted (the whole transaction committed). False when there was nothing to
	// compact (no fetched rows, or a marker-only bucket).
	Compacted bool
	// DeletedCount is the number of source rows actually deleted.
	DeletedCount int64
}

CompactResultsOutcome reports what CompactResults did inside its transaction.

type ConnectionType

type ConnectionType string

ConnectionType represents the type of integration connection.

const (
	ConnectionTypeSlack      ConnectionType = "slack"
	ConnectionTypeDiscord    ConnectionType = "discord"
	ConnectionTypeWebhook    ConnectionType = "webhook"
	ConnectionTypeEmail      ConnectionType = "email"
	ConnectionTypeGoogleChat ConnectionType = "googlechat"
	ConnectionTypeMattermost ConnectionType = "mattermost"
	ConnectionTypeNtfy       ConnectionType = "ntfy"
	// ConnectionTypeGotify is a self-hosted Gotify push server: a stateless
	// HTTP POST to {server_url}/message with the app token in the
	// X-Gotify-Key header, alongside ntfy/Matrix/Mattermost/Pushover in the
	// self-hosted/homelab notification lineup.
	ConnectionTypeGotify     ConnectionType = "gotify"
	ConnectionTypePagerduty  ConnectionType = "pagerduty"
	ConnectionTypePushover   ConnectionType = "pushover"
	ConnectionTypeFreebox    ConnectionType = "freebox"
	ConnectionTypeWebPush    ConnectionType = "webpush"
	ConnectionTypeKubernetes ConnectionType = "kubernetes"
	ConnectionTypeTwilio     ConnectionType = "twilio"
	ConnectionTypeMSTeams    ConnectionType = "msteams"
	// ConnectionTypeMSTeamsBot is the two-way Microsoft Teams bot integration
	// (Azure Bot / Bot Framework). It is deliberately distinct from
	// ConnectionTypeMSTeams, which stays as the zero-infra, one-way Teams
	// Workflow webhook: the two coexist and an org may use either or both.
	ConnectionTypeMSTeamsBot ConnectionType = "msteams-bot"
	// ConnectionTypeMatrix is the org-level Matrix (matrix.org) integration: a
	// stateless HTTP sender to a Matrix room via the Client-Server API,
	// alongside Slack/Discord/ntfy — not the instance-level direct-channel
	// path used by Telegram.
	ConnectionTypeMatrix ConnectionType = "matrix"
	// ConnectionTypeZulip is the Zulip chat integration: a stateless HTTP POST
	// to {site_url}/api/v1/messages via Zulip's bot API, with every lifecycle
	// event of one incident landing in the same topic (see
	// notifications.zulipTopic) so Zulip threads the incident automatically —
	// the same "one thread per incident" outcome Slack works hard to emulate
	// with reverse threads.
	ConnectionTypeZulip ConnectionType = "zulip"
)

Connection types.

type DeliveryDetails

type DeliveryDetails struct {
	// HTTPStatusCode is the HTTP response status (e.g. 200, 503). Zero/omitted
	// for non-HTTP channels or when no response was received.
	HTTPStatusCode int `json:"httpStatusCode,omitempty"`
	// RequestURL is the target URL with the query string and any credentials
	// stripped (host + path only). Never the raw URL.
	RequestURL string `json:"requestUrl,omitempty"`
	// RequestBody is the payload sent to the receiver, capped at
	// DeliveryDetailsBodyCap bytes.
	RequestBody string `json:"requestBody,omitempty"`
	// ResponseBody is the receiver's response body, capped at
	// DeliveryDetailsBodyCap bytes.
	ResponseBody string `json:"responseBody,omitempty"`
	// DurationMs is the wall-clock time of the delivery attempt in milliseconds.
	DurationMs int64 `json:"durationMs,omitempty"`
	// ResponseHeaders is a small allowlisted set of response headers (e.g.
	// Retry-After, Content-Type). Never carries request-side secret headers.
	ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
}

DeliveryDetails holds structured, per-attempt notification delivery artifacts. It is stored as JSON (Postgres jsonb / SQLite text) in incident_notifications.delivery_details. Every field is optional: a channel populates only what it can produce, and a missing field is simply omitted.

Privacy: callers MUST never place the webhook signing secret, an Authorization header, or any other secret into this struct. RequestURL must be stripped of its query string and userinfo before being set.

func (*DeliveryDetails) Scan

func (d *DeliveryDetails) Scan(value any) error

Scan implements sql.Scanner for reading the JSON blob back from either engine.

func (DeliveryDetails) Value

func (d DeliveryDetails) Value() (driver.Value, error)

Value implements driver.Valuer so DeliveryDetails persists as a JSON string on both Postgres (jsonb) and SQLite (text). A nil pointer is handled by the driver as NULL before this is called; on a non-nil empty value it stores "{}".

type DeviceAuthRequest

type DeviceAuthRequest struct {
	bun.BaseModel `bun:"table:device_auth_requests,alias:device_auth_request"`

	UID             string     `bun:"uid,pk,type:varchar(36)"`
	DeviceCode      string     `bun:"device_code,notnull"`
	UserCode        string     `bun:"user_code,notnull"`
	ClientName      string     `bun:"client_name,notnull"`
	Status          string     `bun:"status,notnull"`
	OrganizationUID *string    `bun:"organization_uid"`
	UserUID         *string    `bun:"user_uid"`
	TokenUID        *string    `bun:"token_uid"`
	TokenValue      *string    `bun:"token_value"`
	LastPolledAt    *time.Time `bun:"last_polled_at"`
	ExpiresAt       time.Time  `bun:"expires_at,notnull"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
}

DeviceAuthRequest is a pending OAuth 2.0 Device Authorization Grant request (RFC 8628) opened by a CLI and approved by a human in the dashboard.

DeviceCode is the requesting client's secret and the real capability (32 random bytes); UserCode is the short, human-typed code and therefore the brute-forceable surface — the consent lookup is rate limited on top of the global limiter. TokenValue holds the PAT minted at consent time until the client's next poll picks it up, at which point the whole row is deleted so the token can only ever be delivered once.

OrganizationUID is the org the approving user SELECTED on the consent page, not implicitly their session org: solidping is multi-tenant and the minted PAT is scoped to that choice.

func NewDeviceAuthRequest

func NewDeviceAuthRequest(clientName, deviceCode, userCode string, expiresAt time.Time) *DeviceAuthRequest

NewDeviceAuthRequest builds a pending request with a generated UID. deviceCode and userCode are caller-generated; userCode must already be in canonical (uppercase, dashless) form.

type DeviceAuthResolution

type DeviceAuthResolution struct {
	Status          string
	UserUID         string
	OrganizationUID string
	TokenUID        string
	TokenValue      string
}

DeviceAuthResolution carries the outcome of a consent decision. Status is DeviceAuthStatusApproved or DeviceAuthStatusDenied; the token fields are set only on approval.

type DiscordSettings

type DiscordSettings struct {
	// WebhookURL is the legacy, one-way mode. Kept first and unchanged so a
	// pre-bot row decodes exactly as it always did.
	WebhookURL string `json:"webhook_url,omitempty"`

	// GuildID / GuildName identify the Discord server the bot was installed
	// into. GuildID is the identity the org mapping is keyed on (see
	// organization_providers, ProviderTypeDiscord).
	GuildID   string `json:"guild_id,omitempty"`
	GuildName string `json:"guild_name,omitempty"`

	// BotUserID is the application's own user id inside the guild, used to
	// recognize "this message is ours" on the Gateway.
	BotUserID string `json:"bot_user_id,omitempty"`

	// ChannelID / ChannelName are the default notification destination — the
	// Discord counterpart of SlackSettings.ChannelID.
	ChannelID   string `json:"channel_id,omitempty"`
	ChannelName string `json:"channel_name,omitempty"`

	// InstalledByUserID records the Discord user who performed the install.
	InstalledByUserID string `json:"installed_by_user_id,omitempty"`

	// MentionOnCall makes channel alerts ping the humans the escalation policy
	// would page first (`<@123>`). Zero value is deliberately false so every
	// integration stored before this field existed keeps behaving exactly as
	// before; the bot install flow writes `true` explicitly for new installs.
	MentionOnCall bool `json:"mention_on_call,omitempty"`

	// CommentIngestion selects how inbound Discord thread replies are treated,
	// with the same explicit/all semantics as SlackSettings.CommentIngestion.
	// Zero value ("") means explicit — the safe direction.
	CommentIngestion string `json:"comment_ingestion,omitempty"`
}

DiscordSettings represents Discord-specific settings stored in the Settings JSONB. It covers BOTH Discord modes, and which one a connection is in is a function of the data, never of a separate connection type:

  • Legacy webhook mode: only WebhookURL is set. This is what every Discord integration created before the bot existed looks like, and it keeps working untouched — the sender picks the webhook path whenever the bot fields are absent, with no migration and no re-install.
  • Bot mode: GuildID + ChannelID are set (written by the bot install flow). Threads, message edits, the Acknowledge button, mentions and inbound comments are only available here.

No credential lives in this blob: the bot token is instance-level system config (`auth.discord.bot_token`), exactly like the Teams bot's Entra app secret, so a stolen settings blob grants nothing.

func DiscordSettingsFromJSONMap

func DiscordSettingsFromJSONMap(m JSONMap) (*DiscordSettings, error)

DiscordSettingsFromJSONMap parses DiscordSettings from a JSONMap.

func (*DiscordSettings) IngestsAllThreadReplies

func (ds *DiscordSettings) IngestsAllThreadReplies() bool

IngestsAllThreadReplies reports whether this Discord integration captures every human thread reply as an incident comment. Anything other than an explicit "all" — including an absent value on a pre-existing row — means no.

func (*DiscordSettings) ToJSONMap

func (ds *DiscordSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts DiscordSettings to JSONMap for storage.

func (*DiscordSettings) UsesBot

func (ds *DiscordSettings) UsesBot() bool

UsesBot reports whether this connection is in bot mode. A guild and a target channel are both required: a guild with no channel has nowhere to post, and the sender must fall back to the webhook rather than silently doing nothing.

type DiscoveredCheck

type DiscoveredCheck struct {
	UID                string          `bun:"uid,pk,type:varchar(36)"                         json:"uid"`
	OrganizationUID    string          `bun:"organization_uid,notnull"                        json:"organizationUid"`
	JobUID             string          `bun:"job_uid,notnull"                                 json:"jobUid"`
	Source             DiscoverySource `bun:"source,notnull"                                  json:"source"`
	GroupKey           string          `bun:"group_key,notnull"                               json:"groupKey"`
	GroupLabel         string          `bun:"group_label,notnull"                             json:"groupLabel"`
	Name               string          `bun:"name,notnull"                                    json:"name"`
	Slug               string          `bun:"slug,notnull"                                    json:"slug"`
	Type               string          `bun:"type,notnull"                                    json:"type"`
	Config             json.RawMessage `bun:"config,type:jsonb"                               json:"config"`
	Metadata           json.RawMessage `bun:"metadata,type:jsonb"                             json:"metadata,omitempty"`
	PromotedToCheckUID *string         `bun:"promoted_to_check_uid"                           json:"promotedToCheckUid,omitempty"` //nolint:lll // aligned tag width
	DiscoveredAt       time.Time       `bun:"discovered_at,notnull,default:current_timestamp" json:"discoveredAt"`
	CreatedAt          time.Time       `bun:"created_at,notnull,default:current_timestamp"    json:"createdAt"`
	UpdatedAt          time.Time       `bun:"updated_at,notnull,default:current_timestamp"    json:"updatedAt"`
	DeletedAt          *time.Time      `bun:"deleted_at,soft_delete"                          json:"deletedAt,omitempty"`
}

DiscoveredCheck is one suggested check produced by a discovery scan. Rows are grouped for display by GroupKey (an IP, container ID, workload UID…) — the group is purely a rendering concern; the stored unit is the check itself.

func NewDiscoveredCheck

func NewDiscoveredCheck(
	orgUID, jobUID string, source DiscoverySource,
	groupKey, groupLabel, name, slug, checkType string,
	config, meta json.RawMessage,
) *DiscoveredCheck

NewDiscoveredCheck builds a DiscoveredCheck row from the fully-formed pieces a suggester emits within a group. config and meta may be nil; config defaults to "{}" so the NOT NULL column is satisfied.

type DiscoverySource

type DiscoverySource string

DiscoverySource identifies which discovery mechanism produced a suggested check. It is a named string (matching the ProviderType / JobType convention) so the set of sources is closed in code while staying a plain TEXT column.

const (
	// DiscoverySourceLAN marks a check suggested by the CIDR network scanner.
	DiscoverySourceLAN DiscoverySource = "lan"
	// DiscoverySourceFreebox marks a check suggested via the Freebox LAN browser.
	DiscoverySourceFreebox DiscoverySource = "freebox"
	// DiscoverySourceContainer marks a container found on a configured
	// Docker-compatible host (Docker or Podman).
	DiscoverySourceContainer DiscoverySource = "container"
	// DiscoverySourceKubernetes marks a Deployment/ReplicaSet found on a
	// configured Kubernetes cluster connection.
	DiscoverySourceKubernetes DiscoverySource = "kubernetes"
)

Supported discovery sources. Additional sources (container, kubernetes) are registered by their own specs; the column has no DB CHECK constraint so new values are Go-level only.

type EmailSuppression

type EmailSuppression struct {
	bun.BaseModel `bun:"table:email_suppressions,alias:email_suppression"`

	UID             string    `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string    `bun:"organization_uid,notnull"`
	Email           string    `bun:"email,notnull"`
	CheckUID        *string   `bun:"check_uid"` // NULL = suppresses all checks in the org
	Source          string    `bun:"source,notnull"`
	CreatedAt       time.Time `bun:"created_at,notnull,default:current_timestamp"`
}

EmailSuppression records that a recipient address has opted out of incident/alert emails, either for one specific check (CheckUID set) or for every check in the org (CheckUID nil). Backed by the `email_suppressions` table. Transactional emails (registration, password reset, invitation, password-changed) never consult this table — suppression only applies to incident/alert emails (spec acceptance criterion 6).

func NewEmailSuppression

func NewEmailSuppression(orgUID, email string, checkUID *string, source string) *EmailSuppression

NewEmailSuppression creates a new suppression row with a generated UID. checkUID is nil for an org-wide suppression.

type EntitlementLimits

type EntitlementLimits struct {
	MaxChecks          *int `json:"maxChecks,omitempty"`
	MaxUsers           *int `json:"maxUsers,omitempty"`
	MaxChecksPerMinute *int `json:"maxChecksPerMinute,omitempty"`
	// MaxDeportedAgents caps the org's active deported (private-location)
	// agents across all private regions. nil = unlimited.
	MaxDeportedAgents *int `json:"maxDeportedAgents,omitempty"`
	// MaxCustomDomains caps the org's status pages served on a customer-owned
	// domain. nil = unlimited (self-hosted default); SaaS defaults to 0 and
	// billing raises it per plan.
	MaxCustomDomains *int `json:"maxCustomDomains,omitempty"`
	// MaxSmsPerMonth / MaxCallsPerMonth cap the org's outbound SMS and voice
	// calls per UTC calendar month. nil = unlimited (self-hosted default,
	// bring-your-own Twilio); SaaS defaults to 0 and billing raises it per plan.
	MaxSmsPerMonth   *int `json:"maxSmsPerMonth,omitempty"`
	MaxCallsPerMonth *int `json:"maxCallsPerMonth,omitempty"`
	// MaxWhatsappPerMonth caps the org's outbound WhatsApp template messages
	// per UTC calendar month. nil = unlimited (self-hosted default, the
	// operator brings their own WABA); SaaS defaults to 0 and billing raises
	// it per plan.
	MaxWhatsappPerMonth *int `json:"maxWhatsappPerMonth,omitempty"`
	// MaxSlos caps the org's service-level objectives (spec 2026-08-20-01).
	// nil = unlimited (self-hosted default); SaaS defaults to 2 and billing
	// raises it per plan.
	MaxSlos *int `json:"maxSlos,omitempty"`
	// WhiteLabel is the one non-numeric entitlement: whether the org may drop
	// the "powered by SolidPing" badge from its status pages (spec
	// 2026-08-21-07). It lives here rather than in a sibling struct because
	// this IS the entitlement payload billing writes, and splitting the shape
	// in two would mean a second wire contract to rotate.
	//
	// nil means "use the deployment default" — true self-hosted (nobody should
	// have to pay to unbrand their own instance), false on the SaaS, where
	// paying is what unlocks it. Note the asymmetry with the *int fields: nil
	// there means UNLIMITED, here it means DEFAULT, because a boolean has no
	// "unbounded" reading.
	WhiteLabel *bool `json:"whiteLabel,omitempty"`
}

EntitlementLimits is the quantitative half of an entitlement set. nil = unlimited. JSON tags are the wire format consumed by the API.

Four limits are modeled: MaxChecks (total non-internal checks an org may own, enforced at check creation), MaxUsers (total org members — capped on self-hosted by default), MaxChecksPerMinute (aggregate dispatch rate, capped on SaaS), and MaxDeportedAgents (active deported / private-location agents across all private regions, enforced at enrollment — see AgentCreateAllowed). Adding an optional field to the v1 JSONB payload is backward-compatible — absent keys unmarshal to nil (= unlimited), so no version bump is needed.

MaxUsers is decoded from either the canonical `maxUsers` key or the deprecated `maxSsoUsers` alias (see UnmarshalJSON) so already-deployed billing services and stored v1 rows that still use the old key keep working forever. It is always marshaled back as `maxUsers`.

func (*EntitlementLimits) UnmarshalJSON

func (l *EntitlementLimits) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the limits object, accepting `maxSsoUsers` as a deprecated decode-only alias for `maxUsers`. Sending both is an error. Unknown limit keys are still rejected so typos surface loudly, matching the DisallowUnknownFields contract the PUT decoder used before this type grew a custom unmarshaler.

type EntitlementSource

type EntitlementSource string

EntitlementSource identifies who wrote the row.

const (
	EntitlementSourceDefault    EntitlementSource = "default"
	EntitlementSourceSelfHosted EntitlementSource = "self-hosted"
	EntitlementSourceAdmin      EntitlementSource = "admin"
	EntitlementSourceOrgAdmin   EntitlementSource = "org-admin"
	EntitlementSourceBilling    EntitlementSource = "billing-service"
)

Entitlement source labels.

EntitlementSourceAdmin and EntitlementSourceOrgAdmin look alike and are NOT interchangeable — the difference is which door the write came through, and that decides whether it outranks billing:

  • `admin` is minted ONLY by the instance-level superadmin editor (`PUT /api/v1/system/entitlements/:org`). It is an override: it resolves whole-row (nil = unlimited) and suppresses billing pushes until it is explicitly released.
  • `org-admin` is minted by the org-scoped `PUT /api/v1/orgs/:org/entitlements` when an ORG admin (not a superadmin) writes it — the self-hosted operator's door, gated by `entitlements.admin_writes_enabled`. It behaves exactly as `admin` did before spec 2026-08-26-06: same paid plan weight, ordinary null-fill resolution, and billing's next reconcile overwrites it.

Collapsing the two would let any org admin on a SaaS install that never set SP_ENTITLEMENTS_ADMIN_WRITES grant themselves limits AND lock the billing service out of correcting them, which no superadmin ever authorized.

Migration note: rows already stored as `admin` were written through the org-scoped door and will read as superadmin overrides from now on. There are only a handful, they are visible in the superadmin editor, and releasing one is a single click — so they are left alone rather than rewritten blind.

type EntitlementsPayload

type EntitlementsPayload struct {
	Version int               `json:"version"`
	Source  EntitlementSource `json:"source,omitempty"`
	Limits  EntitlementLimits `json:"limits"`
	// DisplayName / DisplayEmoji are billing-supplied plan identity, shown
	// in the dashboard (e.g. "🚀 Team"). Display-only — never enforced.
	DisplayName  *string `json:"displayName,omitempty"`
	DisplayEmoji *string `json:"displayEmoji,omitempty"`
}

EntitlementsPayload is the structured-by-OSS portion of an org_entitlements row, stored as JSON in the `payload` column. The struct itself is the schema; absent keys mean "use default" and extra keys are silently ignored for forward-compat. The Version field gates shape-migrations at unmarshal time.

func (*EntitlementsPayload) Scan

func (p *EntitlementsPayload) Scan(value any) error

Scan implements sql.Scanner. Empty / NULL values yield a zero-valued payload with the current version stamped in — the resolver will fall back to defaults for absent fields.

func (*EntitlementsPayload) UnmarshalJSON

func (p *EntitlementsPayload) UnmarshalJSON(data []byte) error

UnmarshalJSON probes the version discriminator first and dispatches to the matching shape-migration. v0 (rows written before the version field landed) is treated as v1.

func (*EntitlementsPayload) Value

func (p *EntitlementsPayload) Value() (driver.Value, error)

Value implements driver.Valuer so bun can write the payload as JSON (postgres jsonb / sqlite text) without an explicit hook.

type EscalationPolicy

type EscalationPolicy struct {
	UID                string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID    string     `bun:"organization_uid,notnull"`
	Name               string     `bun:"name,notnull"`
	Description        *string    `bun:"description"`
	RepeatMax          int        `bun:"repeat_max,notnull"`
	RepeatAfterSeconds *int       `bun:"repeat_after_seconds"`
	CreatedAt          time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt          time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt          *time.Time `bun:"deleted_at"`
}

EscalationPolicy is a reusable orchestration of paging steps. Distinct from check_connections (per-check broadcast). The check or its group references one policy via escalation_policy_uid.

func NewEscalationPolicy

func NewEscalationPolicy(orgUID, name string) *EscalationPolicy

NewEscalationPolicy builds a policy with a fresh UID.

type EscalationPolicyStep

type EscalationPolicyStep struct {
	UID          string `bun:"uid,pk,type:varchar(36)"`
	PolicyUID    string `bun:"policy_uid,notnull"`
	Position     int    `bun:"position,notnull"`
	DelaySeconds int    `bun:"delay_seconds,notnull"`
	// SeverityUID points to the severity that decides the channel-set
	// for this step. NULL = "fall back to the org default severity for
	// user/all_admins targets, or the connection's own channel for
	// connection targets". Spec 2026-05-08-03.
	SeverityUID *string   `bun:"severity_uid"`
	CreatedAt   time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt   time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

EscalationPolicyStep is one rung of a policy. Delays are between adjacent steps (see spec): inserting a step in the middle does not require recomputing downstream delays.

func NewEscalationPolicyStep

func NewEscalationPolicyStep(policyUID string, position, delaySeconds int) *EscalationPolicyStep

NewEscalationPolicyStep builds a step row with a fresh UID.

type EscalationPolicyTarget

type EscalationPolicyTarget struct {
	UID        string               `bun:"uid,pk,type:varchar(36)"`
	StepUID    string               `bun:"step_uid,notnull"`
	TargetType EscalationTargetType `bun:"target_type,notnull"`
	TargetUID  *string              `bun:"target_uid"`
	Position   int                  `bun:"position,notnull"`
}

EscalationPolicyTarget is one recipient inside a step. Multiple targets per step fire in parallel.

func NewEscalationPolicyTarget

func NewEscalationPolicyTarget(
	stepUID string, targetType EscalationTargetType, targetUID *string, position int,
) *EscalationPolicyTarget

NewEscalationPolicyTarget builds a target row with a fresh UID.

type EscalationPolicyUpdate

type EscalationPolicyUpdate struct {
	Name               *string
	Description        *string
	RepeatMax          *int
	RepeatAfterSeconds *int

	ClearDescription        bool
	ClearRepeatAfterSeconds bool
}

EscalationPolicyUpdate captures the writable fields. Pointer = optional.

type EscalationTargetType

type EscalationTargetType string

EscalationTargetType is the kind of recipient a policy step pages.

const (
	// EscalationTargetUser pages a specific user via their preferred channels.
	EscalationTargetUser EscalationTargetType = "user"
	// EscalationTargetSchedule pages whoever the on-call resolver returns at fire time.
	EscalationTargetSchedule EscalationTargetType = "schedule"
	// EscalationTargetConnection fires a specific notification connection.
	EscalationTargetConnection EscalationTargetType = "connection"
	// EscalationTargetAllAdmins pages every admin member of the organization.
	EscalationTargetAllAdmins EscalationTargetType = "all_admins"
)

type Event

type Event struct {
	UID             string    `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string    `bun:"organization_uid,notnull"`
	IncidentUID     *string   `bun:"incident_uid"`
	CheckUID        *string   `bun:"check_uid"`
	JobUID          *string   `bun:"job_uid"`
	EventType       EventType `bun:"event_type,notnull"`
	ActorType       ActorType `bun:"actor_type,notnull"`
	// ActorUID is the acting user's UID — this column IS the spec's
	// `actor_user_uid` (2026-08-21-09). It predates that spec as an FK to
	// users(uid); a second column of the same meaning would only create a
	// split brain, so the API exposes it under the `actorUserUid` parameter
	// name while the column keeps its original name.
	ActorUID *string `bun:"actor_uid"`
	Payload  JSONMap `bun:"payload,type:jsonb,nullzero"`
	// SourceIP is the client address the action came from, when the request
	// had one and audit.capture_ip is on. Nil for system-originated events and
	// for deployments that turned IP capture off for GDPR reasons.
	SourceIP *string `bun:"source_ip"`
	// UserAgent is the raw User-Agent header, truncated. Nil when absent.
	UserAgent *string   `bun:"user_agent"`
	CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}

Event represents an audit log entry.

func NewEvent

func NewEvent(orgUID string, eventType EventType, actorType ActorType) *Event

NewEvent creates a new event with generated UID.

type EventType

type EventType string

EventType represents the type of an audit event.

const (
	// EventTypeCheckCreated indicates a check was created.
	EventTypeCheckCreated EventType = "check.created"
	// EventTypeCheckUpdated indicates a check was updated.
	EventTypeCheckUpdated EventType = "check.updated"
	// EventTypeCheckDeleted indicates a check was deleted.
	EventTypeCheckDeleted EventType = "check.deleted"

	// EventTypeIncidentCreated indicates an incident was created.
	EventTypeIncidentCreated EventType = "incident.created"
	// EventTypeIncidentEscalated indicates an incident was escalated.
	EventTypeIncidentEscalated EventType = "incident.escalated"
	// EventTypeIncidentEscalationFailed indicates an escalation step
	// could not be delivered (empty schedule, missing user, etc.). Soft
	// failure — subsequent steps still fire.
	EventTypeIncidentEscalationFailed EventType = "incident.escalation_failed"
	// EventTypeIncidentResolved indicates an incident was resolved.
	EventTypeIncidentResolved EventType = "incident.resolved"
	// EventTypeIncidentReopened indicates an incident was reopened after a relapse.
	EventTypeIncidentReopened EventType = "incident.reopened"
	// EventTypeIncidentRolledUp indicates an already-open incident was
	// retroactively attached to a hard parent's incident and its paging
	// suppressed, because the parent confirmed AFTER it did (spec
	// 2026-08-24-15). Distinct from the silent attachment that happens at
	// incident-open time: this one changes a live incident's paging behavior
	// mid-flight, so the timeline has to say so. Never pages.
	EventTypeIncidentRolledUp EventType = "incident.rolled_up"
	// EventTypeIncidentRollupDetached indicates a suppressed child was
	// un-attached from its rollup parent because the parent resolved AND the
	// child's own check had already recovered (spec
	// 2026-08-31-07-rollup-detach-erases-attribution). paging_suppressed
	// flips false, but caused_by_incident_uid is deliberately kept — it is
	// the post-mortem record of which cascade this incident belonged to.
	// Never pages: the child never paged on the way in, so it does not page
	// on the way out either.
	EventTypeIncidentRollupDetached EventType = "incident.rollup_detached"
	// EventTypeIncidentAcknowledged indicates an incident was acknowledged.
	EventTypeIncidentAcknowledged EventType = "incident.acknowledged"
	// EventTypeIncidentUnacknowledged indicates an acknowledgment was cleared.
	EventTypeIncidentUnacknowledged EventType = "incident.unacknowledged"
	// EventTypeIncidentSnoozed indicates an incident was snoozed until a future time.
	EventTypeIncidentSnoozed EventType = "incident.snoozed"
	// EventTypeIncidentUnsnoozed indicates an incident's snooze was cleared.
	EventTypeIncidentUnsnoozed EventType = "incident.unsnoozed"
	// EventTypeIncidentComment is a free-text, user-authored comment on an
	// incident, ingested from the dashboard or a Slack thread reply. The
	// payload carries `text`, `source` (web|slack) and, for Slack-authored
	// comments, the Slack author attribution (slackUserId, slackUserName,
	// slackTeamId, slackTs). Append-only, like every other event row.
	EventTypeIncidentComment EventType = "incident.comment"

	// EventTypeStatusPageIncidentPublished indicates an incident became visible
	// on a status page (spec 2026-08-19-08). It is deliberately DISTINCT from
	// EventTypeIncidentCreated: an operational incident opening and a
	// customer-visible incident being published are different facts, they
	// happen at different times (the auto-publish debounce sits between them),
	// and a great many incidents never produce the second one at all. A
	// webhook consumer must be able to subscribe to one without the other.
	EventTypeStatusPageIncidentPublished EventType = "statuspage.incident.published"
	// EventTypeStatusPageIncidentUpdated indicates a publication's public
	// title, severity, state or narrative changed.
	EventTypeStatusPageIncidentUpdated EventType = "statuspage.incident.updated"
	// EventTypeStatusPageIncidentResolved indicates a publication was closed
	// (or unpublished). The internal incident.resolved event is unchanged and
	// still fires on its own schedule.
	EventTypeStatusPageIncidentResolved EventType = "statuspage.incident.resolved"

	// EventTypeStatusSubscriberDisabled indicates a webhook/Slack status-page
	// subscription was disabled after repeated delivery failures (spec
	// 2026-08-21-07). Without it the only symptom of a broken webhook is
	// "we stopped getting notifications" — which nobody notices until an
	// incident.
	EventTypeStatusSubscriberDisabled EventType = "statuspage.subscriber.disabled"

	// EventTypeStatusPageCustomDomainDemoted indicates a status page's custom
	// domain stayed unreachable well past its grace window and stopped being
	// served (spec 2026-08-23-03). Until this existed the only symptom was a
	// customer's status page going dark, discovered — during an outage — by
	// the people the page exists to inform. Entering `grace` does NOT emit
	// this: the page is still serving there and paging for it would teach
	// operators to ignore the one that matters.
	EventTypeStatusPageCustomDomainDemoted EventType = "statuspage.custom_domain.demoted"

	// EventTypeStatusUpdateCreated indicates a status update was created.
	EventTypeStatusUpdateCreated EventType = "status_update.created"
	// EventTypeStatusUpdateUpdated indicates a status update was modified.
	EventTypeStatusUpdateUpdated EventType = "status_update.updated"
	// EventTypeStatusUpdateDeleted indicates a status update was soft-deleted.
	EventTypeStatusUpdateDeleted EventType = "status_update.deleted"

	// EventTypeOrgActivationSignupCompleted fires once per org when its
	// initial member is created (the user who provisioned the org).
	EventTypeOrgActivationSignupCompleted EventType = "org.activation.signup_completed"
	// EventTypeOrgActivationFirstCheckCreated fires once per org the first
	// time a check is created for it.
	EventTypeOrgActivationFirstCheckCreated EventType = "org.activation.first_check_created"
	// EventTypeOrgActivationFirstResultReceived fires once per org the first
	// time a check result is recorded for it.
	EventTypeOrgActivationFirstResultReceived EventType = "org.activation.first_result_received"
	// EventTypeOrgActivationFirstNotificationConfigured fires once per org
	// the first time an integration connection is created for it.
	EventTypeOrgActivationFirstNotificationConfigured EventType = "org.activation.first_notification_configured"
	// EventTypeOrgActivationFirstIncidentPaged fires once per org the first
	// time an incident notification is dispatched for it.
	EventTypeOrgActivationFirstIncidentPaged EventType = "org.activation.first_incident_paged"

	// EventTypeAuthLoginSucceeded records a successful authentication. The
	// payload carries `auth_method` — a local first factor (password / ldap /
	// passkey), a named federated connector (oidc / saml / github / …), a
	// composite 2FA form ("password+totp"), or one of the local
	// session-minting paths (invitation / registration / switch_org /
	// org_session). Never any credential.
	//
	// Emitted from auth.Service.startSession, the single point at which a
	// session row is created, so no login path can skip it.
	EventTypeAuthLoginSucceeded EventType = "auth.login_succeeded"
	// EventTypeAuthLoginFailed records a rejected authentication attempt. It is
	// a brute-force amplification vector, so it is the one event type that is
	// NOT written one-row-per-occurrence: repeats of the same (org, email, IP)
	// inside a short window fold into a single row with a `count`, and a
	// per-org hourly ceiling caps how many rows can be created at all. See
	// internal/audit/loginfailed.go.
	EventTypeAuthLoginFailed EventType = "auth.login_failed"
	// EventTypeAuthLogout records a session being ended deliberately.
	EventTypeAuthLogout EventType = "auth.logout"
	// EventTypeAuthTokenCreated records an API token or agent enrollment key
	// being minted. The payload carries the token's NAME and PREFIX only —
	// never the value, which the server itself only ever sees hashed.
	EventTypeAuthTokenCreated EventType = "auth.token_created"
	// EventTypeAuthTokenRevoked records an API token or agent key being revoked.
	EventTypeAuthTokenRevoked EventType = "auth.token_revoked"
	// EventTypeAuthTokenMisuse records a credential being presented by a party
	// it was not issued to — today, an OAuth client asking to revoke a grant
	// that belongs to a different client.
	//
	// Deliberately NOT a variant of auth.token_revoked with a `result` field:
	// auth.token_revoked must mean "a grant was revoked", full stop, or every
	// reader of the trail has to check a discriminator before believing it.
	// This is a different fact — an attempt, not an outcome — and an org
	// should be able to alert on it on its own.
	EventTypeAuthTokenMisuse EventType = "auth.token_misuse"

	// EventTypeMemberInvited records an invitation being sent.
	EventTypeMemberInvited EventType = "member.invited"
	// EventTypeMemberJoined records a membership row being created — whether by
	// an accepted invitation or by an admin adding someone directly.
	EventTypeMemberJoined EventType = "member.joined"
	// EventTypeMemberRemoved records a membership being revoked.
	EventTypeMemberRemoved EventType = "member.removed"
	// EventTypeMemberRoleChanged records a role moving. Emitted only when the
	// role actually changed, so a no-op PATCH does not manufacture an event.
	EventTypeMemberRoleChanged EventType = "member.role_changed"

	// EventTypeIntegrationCreated records a notification integration being added.
	EventTypeIntegrationCreated EventType = "integration.created"
	// EventTypeIntegrationUpdated records an integration being edited. The
	// payload lists changed field names; credential-bearing fields are named
	// but never valued.
	EventTypeIntegrationUpdated EventType = "integration.updated"
	// EventTypeIntegrationDeleted records an integration being removed.
	EventTypeIntegrationDeleted EventType = "integration.deleted"

	// EventTypeEscalationPolicyCreated records an escalation policy being added.
	EventTypeEscalationPolicyCreated EventType = "escalation_policy.created"
	// EventTypeEscalationPolicyUpdated records an escalation policy being edited.
	EventTypeEscalationPolicyUpdated EventType = "escalation_policy.updated"
	// EventTypeEscalationPolicyDeleted records an escalation policy being removed.
	EventTypeEscalationPolicyDeleted EventType = "escalation_policy.deleted"

	// EventTypeOnCallScheduleCreated records an on-call schedule being added.
	EventTypeOnCallScheduleCreated EventType = "oncall_schedule.created"
	// EventTypeOnCallScheduleUpdated records an on-call schedule being edited.
	EventTypeOnCallScheduleUpdated EventType = "oncall_schedule.updated"
	// EventTypeOnCallScheduleDeleted records an on-call schedule being removed.
	EventTypeOnCallScheduleDeleted EventType = "oncall_schedule.deleted"

	// EventTypeStatusPageCreated records a status page being added. Distinct
	// from the statuspage.incident.* family, which is about what a page
	// PUBLISHES rather than about the page's own configuration.
	EventTypeStatusPageCreated EventType = "status_page.created"
	// EventTypeStatusPageUpdated records a status page's configuration changing.
	EventTypeStatusPageUpdated EventType = "status_page.updated"
	// EventTypeStatusPageDeleted records a status page being removed.
	EventTypeStatusPageDeleted EventType = "status_page.deleted"

	// EventTypeMaintenanceWindowCreated records a maintenance window being added.
	EventTypeMaintenanceWindowCreated EventType = "maintenance_window.created"
	// EventTypeMaintenanceWindowUpdated records a maintenance window being edited.
	EventTypeMaintenanceWindowUpdated EventType = "maintenance_window.updated"
	// EventTypeMaintenanceWindowDeleted records a maintenance window being removed.
	EventTypeMaintenanceWindowDeleted EventType = "maintenance_window.deleted"

	// EventTypeConfigApplied records a config-as-code apply. The payload holds
	// the SUMMARY COUNTS (created / updated / deleted / unmanaged) and the
	// manifest name — deliberately never the manifest body, which routinely
	// carries secret references.
	EventTypeConfigApplied EventType = "config.applied"
	// EventTypeOrgSettingsUpdated records an organization-level setting change,
	// as a list of changed field names plus safe scalar values.
	EventTypeOrgSettingsUpdated EventType = "org.settings_updated"
)

type File

type File struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	Name            string     `bun:"name,notnull"`
	MimeType        string     `bun:"mime_type,notnull"`
	Size            int64      `bun:"size,notnull"`
	FileURI         string     `bun:"file_uri,notnull"`
	SHA256          *string    `bun:"sha256"`
	CreatedBy       *string    `bun:"created_by"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`

	// Topic is the ATTACHMENT KEY (spec 2026-08-21-01): a path-like
	// `<entity>/<uid>/<kind>`, e.g.
	// `incidents/9a1eb273-0a95-4d6b-b967-9af076c1f8e8/screenshot`.
	//
	// NIL IS THE NORM. A file that is not an attachment — an org logo, a
	// feedback screenshot — carries no topic and is invisible to every
	// attachment query. The path shape is what makes both accesses cheap on
	// one index: an exact match lists one entity's attachments of one kind, a
	// prefix match reaps everything hanging off an entity when it is deleted.
	Topic *string `bun:"topic"`

	// Details is a free metadata bag for the attachment kind — for a
	// screenshot: capturedAt, region, checkUid, trigger. Unconstrained on
	// purpose so the next attachment kind needs no migration.
	//
	// SECURITY: operator-facing evidence, exactly like incidents.details. It
	// must never be serialized onto a public surface.
	Details JSONMap `bun:"details,type:jsonb,nullzero"`
}

File represents a stored file blob and its metadata. The actual bytes live behind the storage backend identified by FileURI's scheme (file://, s3://).

func NewFile

func NewFile(orgUID, name, mimeType, fileURI string, size int64, createdBy *string) *File

NewFile creates a new file record with a generated UID.

type FreeboxPrivateSettings

type FreeboxPrivateSettings struct {
	AppToken string `json:"appToken"`
}

FreeboxPrivateSettings carries the encrypted secret half of a Freebox connection. The app_token is permanent across Freebox reboots; we only ever store it once, on a successful pairing grant.

type FreeboxSettings

type FreeboxSettings struct {
	BaseURL    string `json:"baseUrl"`              // e.g. "http://mafreebox.freebox.fr"
	AppID      string `json:"appId"`                // "io.solidping"
	DeviceName string `json:"deviceName,omitempty"` // user-visible label on the Freebox admin
	TrackID    int    `json:"trackId,omitempty"`    // only set while pairing; cleared on grant
	Status     string `json:"status,omitempty"`     // pairing | granted | denied | timeout
}

FreeboxSettings represents the public (queryable) side of a Freebox integration connection's Settings JSONB. The matching secret — the permanent app_token granted by the Freebox after LCD approval — lives encrypted in SettingsPrivate under the "appToken" key.

func FreeboxSettingsFromJSONMap

func FreeboxSettingsFromJSONMap(m JSONMap) (*FreeboxSettings, error)

FreeboxSettingsFromJSONMap parses FreeboxSettings from a JSONMap.

func (*FreeboxSettings) ToJSONMap

func (fs *FreeboxSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts FreeboxSettings to JSONMap for storage.

type Incident

type Incident struct {
	UID string `bun:"uid,pk,type:varchar(36)"`
	// Number is the short, per-org, monotonically increasing reference rendered
	// as `#42` in the dashboard, Slack and Telegram. Nobody types a 36-char UUID
	// into a chat on a phone, so every human-facing surface addresses an
	// incident by this instead. Assigned once at creation and never reused —
	// soft-deleted incidents keep theirs.
	Number          int64         `bun:"number,notnull"`
	OrganizationUID string        `bun:"organization_uid,notnull"`
	CheckUID        string        `bun:"check_uid,notnull"`
	Region          *string       `bun:"region"`
	State           IncidentState `bun:"state,notnull"`
	StartedAt       time.Time     `bun:"started_at,notnull"`
	ResolvedAt      *time.Time    `bun:"resolved_at"`
	ResolvedBy      *string       `bun:"resolved_by"`
	ResolutionType  *string       `bun:"resolution_type"`
	EscalatedAt     *time.Time    `bun:"escalated_at"`
	AcknowledgedAt  *time.Time    `bun:"acknowledged_at"`
	AcknowledgedBy  *string       `bun:"acknowledged_by"`
	SnoozedUntil    *time.Time    `bun:"snoozed_until"`
	SnoozedBy       *string       `bun:"snoozed_by"`
	SnoozeReason    *string       `bun:"snooze_reason"`
	FailureCount    int           `bun:"failure_count,notnull"`
	RelapseCount    int           `bun:"relapse_count,notnull"`
	// FlapLevel is the check's FlapCount at the moment this incident opened
	// or last reopened (spec 2026-08-24-05) — the "at what flap level did
	// this page fire" record. 0 = not flapping (first outage in the rolling
	// window). Unlike checks.flap_count this is a point-in-time snapshot, not
	// a live value, so it never needs lazy-reset handling.
	FlapLevel      int        `bun:"flap_level,notnull"`
	LastReopenedAt *time.Time `bun:"last_reopened_at"`
	Title          *string    `bun:"title"`
	Description    *string    `bun:"description"`
	Details        JSONMap    `bun:"details,type:jsonb,nullzero"`
	// CheckGroupUID is set on group incidents — NULL keeps the existing per-check semantics.
	CheckGroupUID *string `bun:"check_group_uid"`
	// CausedByIncidentUID points to the root-cause incident this one was rolled up under.
	CausedByIncidentUID *string `bun:"caused_by_incident_uid"`
	// PagingSuppressed gates notifications and escalation: TRUE = skip.
	PagingSuppressed bool `bun:"paging_suppressed,notnull"`
	// Kind is IncidentKindCheck or IncidentKindSLOBurn.
	Kind string `bun:"kind,notnull"`
	// SLOUID / SLOAlertPolicyUID bind a burn incident to what produced it.
	// Both are NULL on a check incident. Both are `on delete set null`: losing
	// the SLO must not erase the record of the pages it sent.
	SLOUID            *string    `bun:"slo_uid"`
	SLOAlertPolicyUID *string    `bun:"slo_alert_policy_uid"`
	CreatedAt         time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt         time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt         *time.Time `bun:"deleted_at"`
}

Incident represents a period when a check was down.

func NewIncident

func NewIncident(orgUID, checkUID string, startedAt time.Time, title string) *Incident

NewIncident creates a new incident with generated UID.

type IncidentClockUpdate

type IncidentClockUpdate struct {
	FirstFailureAt                  *time.Time
	ClearFirstFailureAt             bool
	FirstSuccessSinceFailureAt      *time.Time
	ClearFirstSuccessSinceFailureAt bool
}

IncidentClockUpdate captures the FirstFailureAt / FirstSuccessSinceFailureAt transition for a single check result. Each field is tri-state: a non-nil pointer sets the column, a true Clear* flag sets it to NULL, and the zero value (nil pointer + false flag) leaves the column untouched.

It is consumed by UpdateCheckStatusAndClocks, which writes the check's status, streak and both incident clocks in one atomic UPDATE.

type IncidentMemberCheck

type IncidentMemberCheck struct {
	IncidentUID    string     `bun:"incident_uid,pk"`
	CheckUID       string     `bun:"check_uid,pk"`
	JoinedAt       time.Time  `bun:"joined_at,notnull,default:current_timestamp"`
	FirstFailureAt time.Time  `bun:"first_failure_at,notnull"`
	LastFailureAt  time.Time  `bun:"last_failure_at,notnull"`
	LastRecoveryAt *time.Time `bun:"last_recovery_at"`
	FailureCount   int        `bun:"failure_count,notnull"`
	// No `default:true` on the tag even though the column has one — see the
	// StatusPage.AutoPublishDelaySeconds note. A member row that is inserted
	// already recovered is legal, and the tag silently rewrote it to failing.
	CurrentlyFailing bool `bun:"currently_failing,notnull"`
}

IncidentMemberCheck tracks a single check's state inside a group incident.

type IncidentMemberUpdate

type IncidentMemberUpdate struct {
	LastFailureAt    *time.Time
	LastRecoveryAt   *time.Time
	FailureCount     *int
	CurrentlyFailing *bool
}

IncidentMemberUpdate represents fields that can be updated on a member row.

type IncidentNotification

type IncidentNotification struct {
	bun.BaseModel `bun:"table:incident_notifications"`

	UID             string  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string  `bun:"organization_uid,notnull,type:varchar(36)"`
	IncidentUID     string  `bun:"incident_uid,notnull,type:varchar(36)"`
	EventType       string  `bun:"event_type,notnull"`
	StepUID         *string `bun:"step_uid,type:varchar(36)"`
	RepeatIndex     *int    `bun:"repeat_index"`
	Source          string  `bun:"source,notnull"`
	UserUID         *string `bun:"user_uid,type:varchar(36)"`
	ConnectionUID   *string `bun:"connection_uid,type:varchar(36)"`
	ChannelType     string  `bun:"channel_type,notnull"`
	Status          string  `bun:"status,notnull"`
	SkipReason      *string `bun:"skip_reason"`
	Error           *string `bun:"error"`
	JobUID          *string `bun:"job_uid,type:varchar(36)"`
	MessageID       *string `bun:"message_id"`
	// DeliveryDetails holds structured per-attempt artifacts (HTTP status,
	// stripped URL, capped request/response bodies, duration). NULL for rows
	// from before the feature and for channels that produce no artifacts.
	DeliveryDetails *DeliveryDetails `bun:"delivery_details,type:jsonb,nullzero"`
	CreatedAt       time.Time        `bun:"created_at,notnull,default:current_timestamp"`
	SentAt          *time.Time       `bun:"sent_at"`
	CanceledAt      *time.Time       `bun:"cancelled_at"` //nolint:misspell // DB column uses British English
	FailedAt        *time.Time       `bun:"failed_at"`
}

IncidentNotification records one dispatch target per event, with full lifecycle tracking (pending → sent | failed | canceled | skipped).

func NewIncidentNotificationForJob

func NewIncidentNotificationForJob(
	orgUID, incidentUID, eventType, source, connectionUID, jobUID, channelType string,
	stepUID *string, repeatIndex *int,
) *IncidentNotification

NewIncidentNotificationForJob builds a pending audit row for a channel-based notification (check_connection or escalation_connection).

func NewIncidentNotificationForUser

func NewIncidentNotificationForUser(
	orgUID, incidentUID, eventType, source, userUID, channelType string,
	stepUID *string, repeatIndex *int,
) *IncidentNotification

NewIncidentNotificationForUser builds a pending audit row for a direct-email escalation target (user, schedule, all_admins paths).

func NewSkippedIncidentNotification

func NewSkippedIncidentNotification(
	orgUID, incidentUID, eventType, source, skipReason string,
	stepUID *string, repeatIndex *int,
) *IncidentNotification

NewSkippedIncidentNotification builds a skipped audit row for paths where no notification is sent (empty schedule, no admins, etc.).

type IncidentNotificationRow

type IncidentNotificationRow struct {
	IncidentNotification

	// Joined from users (non-nil when user_uid IS NOT NULL)
	UserName *string

	// Joined from integration_connections (non-nil when connection_uid IS NOT NULL)
	ConnectionName *string
	ConnectionType *string

	// Joined from incidents for user-scoped queries
	IncidentTitle     *string
	IncidentState     *int
	IncidentStartedAt *time.Time

	// Joined from checks for user-scoped queries
	CheckName *string
}

IncidentNotificationRow is the read-side DTO returned by ListIncidentNotifications. It embeds the base notification row plus joined fields from users and integration_connections that are populated when the respective FK is non-NULL.

type IncidentPublication

type IncidentPublication struct {
	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull"`
	// IncidentUID links the publication to the operational incident. NULL
	// means the publication was authored by hand and tracks nothing.
	IncidentUID   *string `bun:"incident_uid"`
	StatusPageUID string  `bun:"status_page_uid,notnull"`
	// PublicTitle is templated at creation from the resource's PUBLIC display
	// name and is freely editable afterwards. It is never derived from the
	// incident title, which is internal.
	PublicTitle string           `bun:"public_title,notnull"`
	PublicState PublicationState `bun:"public_state,notnull"`
	// Severity is the display-only public badge. nil = no badge.
	Severity *string `bun:"severity"`
	// AutoCreated marks a publication minted by the auto-publish pipeline.
	// It gates group CONSOLIDATION only: the automation never appends "also
	// affecting X" notes to a narrative a person wrote. Auto-RESOLVE and
	// relapse REOPEN are keyed on IncidentUID instead — a publication linked to
	// an incident is in scope of the page's autoResolve policy whether a
	// machine or a person created it, and the two directions must agree or a
	// hand-published entry gets closed by a recovery and never reopened by the
	// relapse (spec 2026-09-02-05).
	AutoCreated bool `bun:"auto_created,notnull"`
	// HumanTouchedAt is stamped the first time a person edits the publication
	// or posts an update on it. It is the whole basis of the `if_untouched`
	// auto-resolve policy.
	//
	// Publishing an incident to a page does NOT stamp it: choosing to make an
	// outage public is not the same as taking over its narrative, and treating
	// it as such turned `if_untouched` into `never` for every hand-published
	// entry.
	HumanTouchedAt *time.Time `bun:"human_touched_at"`
	PublishedAt    time.Time  `bun:"published_at,notnull,default:current_timestamp"`
	ResolvedAt     *time.Time `bun:"resolved_at"`
	// NotifyWindowStart / NotifyWindowCount implement the per-publication
	// subscriber storm cap: at most N fan-out waves per rolling hour. Internal
	// bookkeeping — never serialized publicly.
	NotifyWindowStart *time.Time `bun:"notify_window_start"`
	NotifyWindowCount int        `bun:"notify_window_count,notnull"`
	CreatedAt         time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt         time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt         *time.Time `bun:"deleted_at"`
}

IncidentPublication is the publication overlay: "this incident is visible on this status page, under this customer-readable title, in this state".

It is deliberately NOT the incident row. The operational `incidents` row carries ack/snooze metadata, auto-generated internal titles and probe diagnostics (`details`), none of which may ever reach a customer. Everything on this struct is safe to render publicly EXCEPT the two notify-window counters, which are internal bookkeeping.

func NewIncidentPublication

func NewIncidentPublication(orgUID, statusPageUID, title string, now time.Time) *IncidentPublication

NewIncidentPublication builds a publication row with a generated UID, opening in the `investigating` state.

func (*IncidentPublication) IsResolved

func (p *IncidentPublication) IsResolved() bool

IsResolved reports whether the publication has been closed.

type IncidentPublicationUpdate

type IncidentPublicationUpdate struct {
	PublicTitle     *string
	PublicState     *PublicationState
	Severity        *string
	ClearSeverity   bool
	HumanTouchedAt  *time.Time
	ResolvedAt      *time.Time
	ClearResolvedAt bool
	// NotifyWindowStart / NotifyWindowCount are written together by the storm
	// cap; a nil NotifyWindowStart with NotifyWindowCount set is not a valid
	// combination and is ignored.
	NotifyWindowStart *time.Time
	NotifyWindowCount *int
}

IncidentPublicationUpdate is the tri-state patch applied to a publication row. A nil pointer leaves the column untouched; a Clear* flag writes NULL.

type IncidentState

type IncidentState int

IncidentState represents the state of an incident.

const (
	// IncidentStateActive indicates the incident is ongoing.
	IncidentStateActive IncidentState = 1
	// IncidentStateResolved indicates the incident has been resolved.
	IncidentStateResolved IncidentState = 2
)

type IncidentUpdate

type IncidentUpdate struct {
	Region              *string
	State               *IncidentState
	ResolvedAt          *time.Time
	ResolvedBy          *string
	ResolutionType      *string
	EscalatedAt         *time.Time
	AcknowledgedAt      *time.Time
	AcknowledgedBy      *string
	SnoozedUntil        *time.Time
	SnoozedBy           *string
	SnoozeReason        *string
	FailureCount        *int
	RelapseCount        *int
	FlapLevel           *int
	LastReopenedAt      *time.Time
	Title               *string
	Description         *string
	Details             *JSONMap
	CausedByIncidentUID *string
	PagingSuppressed    *bool

	// Clear* fields set columns to NULL on reopen
	ClearResolvedAt          bool
	ClearResolvedBy          bool
	ClearResolutionType      bool
	ClearAcknowledgedAt      bool
	ClearAcknowledgedBy      bool
	ClearSnoozedUntil        bool
	ClearSnoozedBy           bool
	ClearSnoozeReason        bool
	ClearCausedByIncidentUID bool
}

IncidentUpdate represents fields that can be updated.

type Integration

type Integration struct {
	bun.BaseModel `bun:"table:integrations,alias:integration"`

	UID             string         `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string         `bun:"organization_uid,notnull"`
	Type            ConnectionType `bun:"type,notnull"`
	Name            string         `bun:"name,notnull"`
	// No `default:true` on the tag even though the column has one — see the
	// StatusPage.AutoPublishDelaySeconds note: it made an integration
	// impossible to CREATE disabled (spec 2026-08-30-04). NewIntegration
	// supplies the enabled-by-default.
	Enabled   bool    `bun:"enabled,notnull"`
	IsDefault bool    `bun:"is_default,notnull"`
	Settings  JSONMap `bun:"settings,type:jsonb,notnull"`
	// SettingsPrivate / SettingsPrivateKeys mirror the credential-encryption
	// shape used on Check.Config. Tokens, webhook URLs, API keys live here
	// as an AES-GCM envelope at rest.
	SettingsPrivate     *string    `bun:"settings_private,type:text,nullzero"`
	SettingsPrivateKeys *string    `bun:"settings_private_keys,type:text,nullzero"`
	CreatedAt           time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt           time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt           *time.Time `bun:"deleted_at"`

	// Relations
	Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}

Integration represents a stored, per-org, credentialed connection to a third-party system — Slack, Discord, email, generic webhook, Freebox, etc. It is the umbrella entity; when an integration can receive notifications (CanNotify) it plays the "channel" role. Backed by the `integrations` table.

func NewIntegration

func NewIntegration(orgUID string, connType ConnectionType, name string) *Integration

NewIntegration creates a new integration with generated UID.

type IntegrationUpdate

type IntegrationUpdate struct {
	Name                 *string
	Enabled              *bool
	IsDefault            *bool
	Settings             *JSONMap
	SettingsPrivate      *string
	SettingsPrivateKeys  *string
	ClearSettingsPrivate bool
}

IntegrationUpdate represents fields that can be updated.

type JSONMap

type JSONMap map[string]any

JSONMap represents flexible key-value data stored as JSON.

func GetEffectiveSettings

func GetEffectiveSettings(connection *Integration, checkConnection *CheckConnection) JSONMap

GetEffectiveSettings merges connection defaults with check-level overrides. The check-connection settings take precedence over connection defaults.

func ParameterValue

func ParameterValue(value any) JSONMap

ParameterValue wraps a scalar into the JSON envelope parameters are stored in.

func (*JSONMap) Scan

func (m *JSONMap) Scan(value any) error

Scan implements the sql.Scanner interface for database retrieval.

func (JSONMap) Value

func (m JSONMap) Value() (driver.Value, error)

Value implements the driver.Valuer interface for database storage.

type Job

type Job struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"                        json:"uid"`
	OrganizationUID *string    `bun:"organization_uid"                               json:"organizationUid"`
	Type            string     `bun:"type,notnull"                                   json:"type"`
	Config          JSONMap    `bun:"config,type:jsonb,nullzero"                     json:"config"`
	RetryCount      int        `bun:"retry_count,notnull"                            json:"retryCount"`
	ScheduledAt     time.Time  `bun:"scheduled_at,notnull,default:current_timestamp" json:"scheduledAt"`
	Status          JobStatus  `bun:"status,notnull"                                 json:"status"`
	Output          JSONMap    `bun:"output,type:jsonb,nullzero"                     json:"output"`
	PreviousJobUID  *string    `bun:"previous_job_uid"                               json:"previousJobUid"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"   json:"createdAt"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"   json:"updatedAt"`
	DeletedAt       *time.Time `bun:"deleted_at"                                     json:"deletedAt,omitempty"`
}

Job represents a background task that can be scheduled and executed.

func NewJob

func NewJob(orgUID *string, jobType string) *Job

NewJob creates a new job with generated UID.

type JobStatus

type JobStatus string

JobStatus represents the status of a job.

const (
	// JobStatusPending indicates the job is waiting to be executed.
	JobStatusPending JobStatus = "pending"
	// JobStatusRunning indicates the job is currently executing.
	JobStatusRunning JobStatus = "running"
	// JobStatusSuccess indicates the job completed successfully.
	JobStatusSuccess JobStatus = "success"
	// JobStatusRetried indicates the job failed and a retry was created.
	JobStatusRetried JobStatus = "retried"
	// JobStatusFailed indicates the job failed permanently (no more retries).
	JobStatusFailed JobStatus = "failed"
)

func FinishedJobStatuses

func FinishedJobStatuses() []JobStatus

FinishedJobStatuses returns the terminal job statuses eligible for retention cleanup (jobs_cleanup stage 1 soft-delete). pending/running are deliberately excluded — recovering those is the stuck-job reaper's mandate, not cleanup's. A fresh slice each call keeps callers free to pass it straight into a query.

type JobUpdate

type JobUpdate struct {
	Config         *JSONMap
	RetryCount     *int
	ScheduledAt    *time.Time
	Status         *JobStatus
	Output         *JSONMap
	PreviousJobUID *string
}

JobUpdate represents fields that can be updated.

type KubernetesPrivateSettings

type KubernetesPrivateSettings struct {
	// Token is a bearer token (typically a service-account token) presented to
	// the API server.
	Token string `json:"token,omitempty"`
	// Kubeconfig is a full kubeconfig YAML document that resolves to an API
	// server + credentials. Takes precedence over Token when set.
	Kubeconfig string `json:"kubeconfig,omitempty"`
}

KubernetesPrivateSettings carries the encrypted secret half of a Kubernetes cluster connection. Exactly one of Token or Kubeconfig is set for a remote connection; both are empty for an in-cluster connection.

func KubernetesPrivateSettingsFromMap

func KubernetesPrivateSettingsFromMap(decrypted map[string]any) *KubernetesPrivateSettings

KubernetesPrivateSettingsFromMap parses the decrypted secret half from a plaintext map (the shape returned by credentials.DecryptForOrg or the plaintext fallback on Settings).

type KubernetesSettings

type KubernetesSettings struct {
	// APIServer is the cluster API server URL (e.g. "https://10.0.0.1:6443").
	// Empty when InCluster is true.
	APIServer string `json:"apiServer,omitempty"`
	// CACert is the PEM-encoded cluster CA bundle used to verify the API
	// server certificate. Optional; ignored when InsecureSkipTLSVerify is set.
	CACert string `json:"caCert,omitempty"`
	// InsecureSkipTLSVerify disables API-server certificate verification.
	// Use only for clusters with self-signed certs you cannot pin.
	InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
	// InCluster resolves the connection from the pod's mounted service-account
	// token (rest.InClusterConfig). Only works when solidping runs inside the
	// target cluster; needs no stored secret.
	InCluster bool `json:"inCluster,omitempty"`
}

KubernetesSettings is the public (queryable) side of a Kubernetes cluster connection's Settings JSONB. The matching secret — a bearer token or a pasted kubeconfig — lives encrypted in SettingsPrivate under the "token" / "kubeconfig" keys (see KubernetesPrivateSettings). An in-cluster connection (InCluster=true) stores no secret and is resolved via the mounted service account at connect time.

func KubernetesSettingsFromJSONMap

func KubernetesSettingsFromJSONMap(m JSONMap) (*KubernetesSettings, error)

KubernetesSettingsFromJSONMap parses KubernetesSettings from a JSONMap.

func (*KubernetesSettings) ToJSONMap

func (ks *KubernetesSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts KubernetesSettings to JSONMap for storage.

type Label

type Label struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	Key             string     `bun:"key,notnull"`
	Value           string     `bun:"value,notnull"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

Label represents a key-value pair for categorizing checks.

func NewLabel

func NewLabel(orgUID, key, value string) *Label

NewLabel creates a new label with generated UID.

type LabelSuggestion

type LabelSuggestion struct {
	Value string
	Count int
}

LabelSuggestion is one row of an autocomplete query: either a label key (when listing distinct keys) or a label value (when listing distinct values for a given key), together with the number of distinct checks carrying it.

type ListChecksFilter

type ListChecksFilter struct {
	Labels          map[string]string // key:value pairs for AND filtering
	CheckGroupUID   *string           // filter by check group UID; "none" = ungrouped checks only
	Query           string            // search term for name/slug (case-insensitive substring)
	Types           []string          // optional filter by check type (e.g. ["ssh"]); empty = every type
	Internal        *string           // "true", "false", or "all" — filter by internal status
	Statuses        []CheckStatus     // optional filter by current status (up/down/etc.)
	Limit           int               // max results to return (0 = no limit)
	CursorCreatedAt *time.Time        // cursor: created_at of last item from previous page
	CursorUID       *string           // cursor: uid of last item from previous page

	// SortByGroup opts into display-order pagination (sort=group): group
	// sort_order asc, ungrouped last, then created_at DESC / uid DESC within a
	// bucket. Off = the default created_at DESC / uid DESC ordering.
	SortByGroup bool
	// CursorGroupSortKey is the effective group sort key of the last item from
	// the previous page — the leading component of the composite sort=group
	// cursor. Only set alongside CursorCreatedAt/CursorUID when SortByGroup.
	CursorGroupSortKey *int64

	// SortByTargetHost opts into the by-host-view pagination (sort=targetHost):
	// targetHost sort key ascending (checks with none of host/url/target last),
	// then name ascending, then uid ascending as the final tiebreaker.
	SortByTargetHost bool
	// CursorTargetHostKey and CursorTargetHostName are the leading two
	// components of the composite sort=targetHost cursor (the third, uid, reuses
	// CursorUID). Only set alongside CursorUID when SortByTargetHost.
	CursorTargetHostKey  *string
	CursorTargetHostName *string
}

ListChecksFilter provides filtering options for listing checks.

type ListEventsFilter

type ListEventsFilter struct {
	OrganizationUID string      // Required: organization scope
	IncidentUID     *string     // Optional: filter by incident UID
	CheckUID        *string     // Optional: filter by check UID
	EventTypes      []EventType // Optional: filter by exact event types
	// EventTypePrefixes filters by event-type FAMILY: "auth" matches every
	// auth.* type. Combined with EventTypes as an OR (either predicate may
	// admit a row); empty means "no family restriction".
	EventTypePrefixes []string
	// ExcludeEventTypePrefixes hides whole families. This is the server-side
	// half of "auth events are admin-only": a non-admin caller always carries
	// ExcludeEventTypePrefixes=["auth"], so neither an unfiltered listing nor
	// an explicit ?type=auth can leak them.
	ExcludeEventTypePrefixes []string
	ActorType                *ActorType // Optional: filter by actor type
	// ActorUID filters to the events one user caused (API: actorUserUid).
	ActorUID *string
	// TargetUID filters to the events about one object, matched against the
	// payload's target_uid. Stored in JSON rather than a column because the
	// target is polymorphic (a check, a policy, a token, a member…), so this
	// is a payload predicate rather than an indexed one.
	TargetUID *string
	// TargetType filters to one kind of object ("integration", "member", …).
	TargetType *string
	// TargetSearch is the operator-facing free-text target filter: it matches
	// an exact target_uid OR a case-insensitive substring of the target_name
	// captured on the event.
	//
	// Both halves, because an operator has one box and two things they might
	// paste into it — the UID from a URL, or the name they remember. A
	// UID-only filter behind a box labeled "name or UID" is a promise the
	// query silently breaks.
	TargetSearch *string
	// SourceIP filters to the events that came from one client address.
	//
	// ADMIN-ONLY at the service layer, for the same reason the column is
	// withheld from non-admins: without that gate a viewer who cannot SEE the
	// addresses could still use this filter as an oracle — ask for an IP, get
	// a non-empty page, and you have confirmed a colleague was working from
	// it. A withheld column plus an open filter is not a gate.
	SourceIP *string
	Since    *time.Time // Optional: events created after this time
	Until    *time.Time // Optional: events created before this time

	// Cursor-based pagination
	CursorTimestamp *time.Time // Optional: events with created_at < this timestamp
	CursorUID       *string    // Optional: for same timestamp, events with UID < this

	Limit int // Optional: pagination limit
}

ListEventsFilter provides filtering options for listing events.

type ListFilesFilter

type ListFilesFilter struct {
	Q string
	// Topic restricts the listing to attachments with this EXACT topic.
	// Mutually exclusive with TopicPrefix in practice; both may be set and
	// both are then applied.
	Topic string
	// TopicPrefix restricts the listing to attachments whose topic starts with
	// this string — the entity-scoped form, e.g. `incidents/<uid>/`.
	TopicPrefix string
	Offset      int
	Limit       int
}

ListFilesFilter provides filtering options for listing files.

type ListIncidentPublicationsFilter

type ListIncidentPublicationsFilter struct {
	OrganizationUID string
	StatusPageUID   string
	IncidentUID     string
	// State filters to a single public state when non-empty.
	State string
	// ActiveOnly restricts to publications that are not resolved.
	ActiveOnly bool
	Limit      int
	Offset     int
}

ListIncidentPublicationsFilter narrows a publication listing.

type ListIncidentsFilter

type ListIncidentsFilter struct {
	OrganizationUID string          // Required: organization scope
	CheckUIDs       []string        // Optional: filter by check UIDs
	CheckGroupUID   string          // Optional: filter to group incidents on this group
	MemberCheckUID  string          // Optional: incidents that contain this check (per-check or group member)
	States          []IncidentState // Optional: filter by states (active, resolved)
	// Kinds restricts to these incident kinds. EMPTY MEANS ALL, on purpose:
	// the dashboard's incident list is meant to show burn alerts alongside
	// check outages. Callers that compute DOWNTIME from incidents (availability,
	// SLO status, uptime reports) pass IncidentKindCheck, or a burn alert would
	// count as downtime against the very objective that produced it.
	Kinds          []string
	Since          *time.Time // Optional: incidents started after this time
	Until          *time.Time // Optional: incidents started before this time
	HideSuppressed bool       // Optional: hide rolled-up (paging-suppressed) incidents
	CausedByUID    string     // Optional: only incidents whose caused_by_incident_uid equals this
	// AckedOnly returns only incidents with non-NULL acknowledged_at (and
	// expired snoozes, since snoozed-but-expired is just acked again). Set by
	// the handler when ?state=acked is requested.
	AckedOnly bool
	// SnoozedOnly returns only incidents currently snoozed (snoozed_until in
	// the future). Set by the handler when ?state=snoozed is requested.
	SnoozedOnly bool

	// Cursor-based pagination
	CursorTimestamp *time.Time // Optional: incidents with started_at < this timestamp
	CursorUID       *string    // Optional: for same timestamp, incidents with UID < this

	Limit int // Optional: pagination limit
}

ListIncidentsFilter provides filtering options for listing incidents.

type ListIntegrationsFilter

type ListIntegrationsFilter struct {
	OrganizationUID string
	Type            *ConnectionType
	Enabled         *bool
}

ListIntegrationsFilter represents filter options for listing integrations.

type ListMaintenanceWindowsFilter

type ListMaintenanceWindowsFilter struct {
	Status string // "active", "upcoming", "past", or "" for all
	Limit  int    // max results (0 = no limit)
}

ListMaintenanceWindowsFilter provides filtering options for listing maintenance windows.

type ListMembershipRequestsFilter

type ListMembershipRequestsFilter struct {
	OrganizationUID string
	UserUID         string
	Status          MembershipRequestStatus
	Limit           int
	Offset          int
}

ListMembershipRequestsFilter narrows a list query.

type ListOrgEntitlementAuditsFilter

type ListOrgEntitlementAuditsFilter struct {
	OrganizationUID string
	Limit           int
	Offset          int
}

ListOrgEntitlementAuditsFilter narrows an audit list query.

type ListResultsFilter

type ListResultsFilter struct {
	OrganizationUID string   // Required: organization scope
	CheckUIDs       []string // Optional: filter by multiple check UIDs
	CheckTypes      []string // Optional: filter by check types (requires join with checks table)
	Regions         []string // Optional: filter by multiple regions
	PeriodTypes     []string // Optional: filter by multiple period_types ('raw', 'hour', 'day', 'month')
	Statuses        []int    // Optional: filter by multiple status integers (inclusion)
	// ExcludeStatuses drops rows whose status is in this set (NULL status is
	// never excluded). Used by aggregation work-discovery to skip buckets whose
	// only rows are lifecycle markers (created/running), which would otherwise
	// re-aggregate into degenerate rollups forever (poison-pill loop, spec
	// 2026-07-11-16). Applied independently of Statuses.
	ExcludeStatuses []int
	// RequireCheckExists, when true, drops rows whose check_uid no longer has a
	// row in `checks` (`check_uid IN (SELECT uid FROM checks)`). Soft-deleted
	// checks still have a row and keep their history — only rows orphaned by a
	// hard delete are excluded. Used by aggregation work-discovery so a single
	// FK-orphan raw row can't become a deterministic poison pill that fails the
	// rollup INSERT and permanently halts the org's aggregation (spec
	// 2026-07-12-01 §2). A no-op on Postgres by FK construction; implemented on
	// both backends for parity and defense in depth.
	RequireCheckExists bool
	PeriodStartAfter   *time.Time // Optional: filter period_start >= this value
	// Optional: filter period_start < this value (filters by period_start, not period_end)
	PeriodEndBefore *time.Time

	// Cursor-based pagination
	CursorTimestamp *time.Time // Optional: results with period_start < this timestamp
	CursorUID       *string    // Optional: for same timestamp, results with UID < this

	// Limit
	Limit int // Optional: pagination limit

	// Include check info (for joining with checks table to get slug/name)
	IncludeCheckInfo bool // Optional: whether to join with checks table

	// SkipBlobs, when true, drops the two JSON blob columns (`metrics`,
	// `output`) from the SELECT projection: the scanned rows come back with
	// Metrics and Output nil regardless of what is stored. Only for consumers
	// that need status/counts/duration and never render a blob (uptime bars,
	// badges, status-page recent results) — it trims per-request I/O on the
	// largest table in the system (spec 2026-07-24-02 §5). Default (false)
	// keeps the full row, so every other reader is unaffected.
	SkipBlobs bool
}

ListResultsFilter provides filtering options for listing results.

type ListResultsResponse

type ListResultsResponse struct {
	Results []*Result // The result records
}

ListResultsResponse wraps a page of results. There is deliberately no total count, next-cursor, or has-more field here: `results` is the largest table in the system, so this endpoint is cursor-paginated and the DB layer never computes any of the three (spec 2026-08-18-04). The service layer derives its own cursor/has-more by over-fetching one extra row (internal/handlers/results/service.go), which is why those fields would be dead weight on this struct even if the DB layer did populate them.

type ListSLOsFilter

type ListSLOsFilter struct {
	// CheckUID, when set, restricts to SLOs scoped directly to that check.
	CheckUID string
	// EnabledOnly restricts to enabled SLOs.
	EnabledOnly bool
	Limit       int
}

ListSLOsFilter provides filtering options for listing SLOs.

type ListSeveritiesFilter

type ListSeveritiesFilter struct {
	OrganizationUID string
}

ListSeveritiesFilter narrows a list query.

type ListSupportThreadsFilter

type ListSupportThreadsFilter struct {
	Status  string
	Channel string
	Query   string
	Limit   int
}

ListSupportThreadsFilter configures the thread listing.

type MSTeamsBotSettings

type MSTeamsBotSettings struct {
	TenantID   string `json:"tenant_id"`
	TenantName string `json:"tenant_name,omitempty"`
	// BotID is the bot's own Bot Framework user id inside this tenant
	// (`28:<app-id>`), used to recognize "the member added is us".
	BotID string `json:"bot_id,omitempty"`
	// AppID records which Entra app performed the install, so a credential
	// rotation that changes the app id is visible rather than silent.
	AppID string `json:"app_id,omitempty"`
	// ServiceURL is the tenant's regional Bot Connector base URL, captured at
	// install and refreshed on every inbound activity.
	ServiceURL string `json:"service_url,omitempty"`
	// ChannelID / ChannelName / TeamID are the default notification
	// destination — the Teams counterpart of SlackSettings.ChannelID.
	ChannelID   string `json:"channel_id,omitempty"`
	ChannelName string `json:"channel_name,omitempty"`
	TeamID      string `json:"team_id,omitempty"`
	DisplayName string `json:"display_name,omitempty"`

	InstalledByUserID string `json:"installed_by_user_id,omitempty"`
	// UninstalledAt is set (RFC3339) when the tenant removes the app. The row
	// is kept so the dashboard can render "uninstalled — reinstall to resume"
	// instead of the integration silently vanishing.
	UninstalledAt string `json:"uninstalled_at,omitempty"`

	Destinations []MSTeamsDestination `json:"destinations,omitempty"`
}

MSTeamsBotSettings represents the settings JSONB of a `msteams-bot` connection. It is the Teams analog of SlackSettings: TenantID replaces TeamID as the workspace identity, and Destinations holds the conversation references captured when the bot was added to a team/channel (Teams has no "list all channels" API for a bot, so destinations are accumulated from install/conversationUpdate activities rather than fetched on demand).

No credential lives here: the Entra app id/secret are instance-level system config (SaaS: SolidPing's multi-tenant app; self-hosted: the operator's own), so a stolen settings blob grants nothing. `app_secret` is still registered in credentials.ConnectionSecretFields as defense in depth for any future per-connection override.

func MSTeamsBotSettingsFromJSONMap

func MSTeamsBotSettingsFromJSONMap(m JSONMap) (*MSTeamsBotSettings, error)

MSTeamsBotSettingsFromJSONMap parses MSTeamsBotSettings from a JSONMap.

func (*MSTeamsBotSettings) FindDestination

func (s *MSTeamsBotSettings) FindDestination(conversationID string) *MSTeamsDestination

FindDestination returns the captured conversation reference with this id.

func (*MSTeamsBotSettings) HasDestination

func (s *MSTeamsBotSettings) HasDestination(conversationID string) bool

HasDestination reports whether conversationID is one of the conversation references this connection actually captured from Bot Framework.

This is the authorization rule for every destination selection, wherever it is made: the dashboard PATCH, the per-check override, and the in-band `config default-channel` command all funnel through it. A Teams conversation id is discoverable (it appears in "Get link to channel" URLs), so treating one as usable just because a client named it would let an org post into a channel belonging to a different tenant — the same "asserted vs proven identifier" mistake that made tenant_id exploitable.

func (*MSTeamsBotSettings) ToJSONMap

func (s *MSTeamsBotSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts MSTeamsBotSettings to JSONMap for storage.

type MSTeamsDestination

type MSTeamsDestination struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	TeamID     string `json:"team_id,omitempty"`
	TeamName   string `json:"team_name,omitempty"`
	ServiceURL string `json:"service_url,omitempty"`
	// Type is "channel" today. Personal-scope DMs are phase 2 and would add
	// "personal" here without changing the shape.
	Type string `json:"type,omitempty"`
}

MSTeamsDestination is a single captured Bot Framework conversation reference — the Teams equivalent of a Slack channel in the destinations picker. `ID` is the Bot Framework conversation id (for a channel-scoped install this is the channel's thread id), `ServiceURL` is the regional Bot Connector base URL that conversation must be addressed through, and `TeamID` / `TeamName` carry the owning team so the dashboard can group channels per team.

type MSTeamsSettings

type MSTeamsSettings struct {
	WebhookURL string `json:"webhook_url"`
}

MSTeamsSettings represents Microsoft Teams-specific settings stored in the Settings JSONB. WebhookURL is the Teams Workflow ("When a Teams webhook request is received") URL — the legacy Office 365 Connector format is not supported. Uses webhook_url (matching Discord, see DiscordSettings above) end-to-end on both the frontend form and this Go struct tag; unlike googlechat/mattermost, there is no key mismatch here by design.

func MSTeamsSettingsFromJSONMap

func MSTeamsSettingsFromJSONMap(m JSONMap) (*MSTeamsSettings, error)

MSTeamsSettingsFromJSONMap parses MSTeamsSettings from a JSONMap.

func (*MSTeamsSettings) ToJSONMap

func (ms *MSTeamsSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts MSTeamsSettings to JSONMap for storage.

type MaintenanceWindow

type MaintenanceWindow struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	Title           string     `bun:"title,notnull"`
	Description     *string    `bun:"description"`
	StartAt         time.Time  `bun:"start_at,notnull"`
	EndAt           time.Time  `bun:"end_at,notnull"`
	Recurrence      string     `bun:"recurrence,notnull"`
	RecurrenceEnd   *time.Time `bun:"recurrence_end"`
	CreatedBy       *string    `bun:"created_by"`
	CreatedAt       time.Time  `bun:"created_at,notnull"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

MaintenanceWindow represents a scheduled maintenance window for an organization.

func NewMaintenanceWindow

func NewMaintenanceWindow(orgUID, title string, startAt, endAt time.Time) *MaintenanceWindow

NewMaintenanceWindow creates a new maintenance window with generated UID.

type MaintenanceWindowCheck

type MaintenanceWindowCheck struct {
	UID                  string    `bun:"uid,pk,type:varchar(36)"`
	MaintenanceWindowUID string    `bun:"maintenance_window_uid,notnull"`
	CheckUID             *string   `bun:"check_uid"`
	CheckGroupUID        *string   `bun:"check_group_uid"`
	CreatedAt            time.Time `bun:"created_at,notnull"`
}

MaintenanceWindowCheck represents the association between a maintenance window and a check or check group.

type MaintenanceWindowUpdate

type MaintenanceWindowUpdate struct {
	Title         *string
	Description   *string
	StartAt       *time.Time
	EndAt         *time.Time
	Recurrence    *string
	RecurrenceEnd *time.Time
}

MaintenanceWindowUpdate represents fields that can be updated on a maintenance window.

type MemberRole

type MemberRole string

MemberRole represents a user's role in an organization.

const (
	MemberRoleOwner  MemberRole = "owner"
	MemberRoleAdmin  MemberRole = "admin"
	MemberRoleUser   MemberRole = "user"
	MemberRoleViewer MemberRole = "viewer"
)

Member roles, from most to least privileged. The ordering is meaningful: owner > admin > user > viewer. Use MemberRole.AtLeast rather than equality whenever a call site gates on "at least this much privilege" — an owner must pass every admin gate.

func (MemberRole) AtLeast

func (r MemberRole) AtLeast(minRole MemberRole) bool

AtLeast reports whether the role carries at least the privilege of min. An unknown role never satisfies any gate; an unknown min is never satisfied either (it ranks 0 but a role must still be valid to pass).

func (MemberRole) IsValid

func (r MemberRole) IsValid() bool

IsValid reports whether the role is one of the known member roles.

func (MemberRole) Rank

func (r MemberRole) Rank() int

Rank returns the role's privilege rank. Unknown roles rank below every valid role.

type MembershipRequest

type MembershipRequest struct {
	UID             string                  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string                  `bun:"organization_uid,notnull"`
	UserUID         string                  `bun:"user_uid,notnull"`
	Message         *string                 `bun:"message"`
	Status          MembershipRequestStatus `bun:"status,notnull"`
	DecisionReason  *string                 `bun:"decision_reason"`
	DecidedAt       *time.Time              `bun:"decided_at"`
	DecidedByUID    *string                 `bun:"decided_by_uid"`
	CreatedAt       time.Time               `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time               `bun:"updated_at,notnull,default:current_timestamp"`

	Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
	User         *User         `bun:"rel:belongs-to,join:user_uid=uid"`
	DecidedBy    *User         `bun:"rel:belongs-to,join:decided_by_uid=uid"`
}

MembershipRequest represents a user's ask to join an organization.

func NewMembershipRequest

func NewMembershipRequest(orgUID, userUID string, message *string) *MembershipRequest

NewMembershipRequest creates a new pending request with a generated UID.

type MembershipRequestStatus

type MembershipRequestStatus string

MembershipRequestStatus is the lifecycle state of a membership request.

const (
	MembershipRequestStatusPending   MembershipRequestStatus = "pending"
	MembershipRequestStatusApproved  MembershipRequestStatus = "approved"
	MembershipRequestStatusRejected  MembershipRequestStatus = "rejected"
	MembershipRequestStatusCancelled MembershipRequestStatus = "canceled"
)

Membership request statuses.

type OAuthClient

type OAuthClient struct {
	bun.BaseModel `bun:"table:oauth_clients,alias:oauth_client"`

	UID          string    `bun:"uid,pk,type:varchar(36)"`
	ClientID     string    `bun:"client_id,notnull"`
	SecretHash   *string   `bun:"secret_hash"`
	ClientName   string    `bun:"client_name"`
	RedirectURIs []string  `bun:"redirect_uris,type:jsonb,nullzero"`
	GrantTypes   []string  `bun:"grant_types,type:jsonb,nullzero"`
	Scopes       []string  `bun:"scopes,type:jsonb,nullzero"`
	IsPublic     bool      `bun:"is_public,notnull"`
	CreatedAt    time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt    time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

OAuthClient is a registered OAuth 2.1 client for the MCP authorization server. Native MCP clients (Claude Desktop, mcp-remote) register dynamically (RFC 7591) as public clients (no secret) using PKCE + loopback redirects; confidential clients store a hashed secret. Redirect URIs are stored as a JSON array and validated on every /authorize.

func NewOAuthClient

func NewOAuthClient(clientID string) *OAuthClient

NewOAuthClient builds a new client row with a generated UID.

type Occurrence

type Occurrence struct {
	StartAt time.Time `json:"startAt"`
	EndAt   time.Time `json:"endAt"`
}

Occurrence is one concrete activation of a (possibly recurring) maintenance window.

func NextOccurrences

func NextOccurrences(window *MaintenanceWindow, from time.Time, n int) []Occurrence

NextOccurrences returns up to n occurrences whose end is at/after from, in chronological order, honoring RecurrenceEnd. For recurrence "none" it returns the single window when not yet past, else an empty slice. The currently-active occurrence (if any) is included as the first entry.

type OnCallSchedule

type OnCallSchedule struct {
	UID             string       `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string       `bun:"organization_uid,notnull"`
	Name            string       `bun:"name,notnull"`
	Description     *string      `bun:"description"`
	Timezone        string       `bun:"timezone,notnull"`
	RotationType    RotationType `bun:"rotation_type,notnull"`
	HandoffTime     string       `bun:"handoff_time,notnull"` // HH:MM in schedule timezone
	HandoffWeekday  *int         `bun:"handoff_weekday"`      // 0–6 (Mon=0); required for weekly
	StartAt         time.Time    `bun:"start_at,notnull"`     // First handoff in the rotation cycle
	ICalSecret      *string      `bun:"ical_secret"`          // NULL = feed disabled
	CreatedAt       time.Time    `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time    `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time   `bun:"deleted_at"`
}

OnCallSchedule is a rotation: a list of users plus a cadence and timezone that, together with a moment in time, resolves to one user (the "currently on call"). The schedule itself does not page anyone — escalation policies (separate spec) consume schedules at fan-out time.

func NewOnCallSchedule

func NewOnCallSchedule(orgUID, name, timezone string, rotation RotationType) *OnCallSchedule

NewOnCallSchedule builds a schedule with a fresh UID; caller fills the remaining fields.

type OnCallScheduleOverride

type OnCallScheduleOverride struct {
	UID          string    `bun:"uid,pk,type:varchar(36)"`
	ScheduleUID  string    `bun:"schedule_uid,notnull"`
	UserUID      string    `bun:"user_uid,notnull"`
	StartAt      time.Time `bun:"start_at,notnull"` // inclusive
	EndAt        time.Time `bun:"end_at,notnull"`   // exclusive
	Reason       *string   `bun:"reason"`
	CreatedByUID *string   `bun:"created_by_uid"`
	CreatedAt    time.Time `bun:"created_at,notnull,default:current_timestamp"`
}

OnCallScheduleOverride is a time-bounded replacement of the rotation's next on-call user. Overlapping overrides resolve to the most recently created (documented behavior, not validated at write time).

func NewOnCallScheduleOverride

func NewOnCallScheduleOverride(
	scheduleUID, userUID string, startAt, endAt time.Time,
) *OnCallScheduleOverride

NewOnCallScheduleOverride builds an override row with a fresh UID.

type OnCallScheduleUpdate

type OnCallScheduleUpdate struct {
	Name           *string
	Description    *string
	Timezone       *string
	RotationType   *RotationType
	HandoffTime    *string
	HandoffWeekday *int
	StartAt        *time.Time
	ICalSecret     *string

	ClearDescription    bool
	ClearHandoffWeekday bool
	ClearICalSecret     bool
}

OnCallScheduleUpdate captures the writable fields of a schedule. Pointer semantics: nil = unchanged, non-nil = set.

type OnCallScheduleUser

type OnCallScheduleUser struct {
	UID         string    `bun:"uid,pk,type:varchar(36)"`
	ScheduleUID string    `bun:"schedule_uid,notnull"`
	UserUID     string    `bun:"user_uid,notnull"`
	Position    int       `bun:"position,notnull"`
	CreatedAt   time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt   time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

OnCallScheduleUser is one row in a schedule's ordered roster.

func NewOnCallScheduleUser

func NewOnCallScheduleUser(scheduleUID, userUID string, position int) *OnCallScheduleUser

NewOnCallScheduleUser builds a roster entry with a fresh UID.

type OrgEntitlementAudit

type OrgEntitlementAudit struct {
	UID             string    `bun:"uid,pk,type:varchar(36)"                      json:"uid"`
	OrganizationUID string    `bun:"organization_uid,notnull"                     json:"organizationUid"`
	Source          string    `bun:"source,notnull"                               json:"source"`
	Actor           string    `bun:"actor,notnull"                                json:"actor"`
	BeforeSnapshot  JSONMap   `bun:"before_snapshot,type:jsonb,nullzero"          json:"beforeSnapshot,omitempty"`
	AfterSnapshot   JSONMap   `bun:"after_snapshot,type:jsonb,notnull"            json:"afterSnapshot"`
	Reason          *string   `bun:"reason"                                       json:"reason,omitempty"`
	CreatedAt       time.Time `bun:"created_at,notnull,default:current_timestamp" json:"createdAt"`
}

OrgEntitlementAudit records one write to org_entitlements. The before snapshot is nil on the first row for an org; after is always populated.

The JSON tags are load-bearing: this model is serialized straight onto the audit-listing endpoint, and without them Go would emit Go field names while the OpenAPI spec (and the client generated from it) promise camelCase.

func NewOrgEntitlementAudit

func NewOrgEntitlementAudit(
	orgUID, source, actor string,
	before JSONMap, after JSONMap, reason *string,
) *OrgEntitlementAudit

NewOrgEntitlementAudit builds a fresh audit row.

type OrgEntitlements

type OrgEntitlements struct {
	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull,unique"`

	Payload EntitlementsPayload `bun:"payload,type:jsonb,notnull"`

	ExternalRef  *string    `bun:"external_ref"`
	ExpiresAt    *time.Time `bun:"expires_at"`
	LastSyncedAt *time.Time `bun:"last_synced_at"`
	Metadata     JSONMap    `bun:"metadata,type:jsonb,nullzero"`

	CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

OrgEntitlements is one row per org. Limits, features, and source live inside Payload; absence inside the payload means "use the in-code default" (resolution happens in the entitlements service, never stored). ExternalRef / ExpiresAt / LastSyncedAt are kept as columns because they are queried or indexed at the SQL level.

func NewOrgEntitlements

func NewOrgEntitlements(orgUID string, source EntitlementSource) *OrgEntitlements

NewOrgEntitlements builds a fresh row with the given source. The payload is initialized with the current schema version and empty limits/features so the resolver merges in defaults.

type OrgUsageCounter

type OrgUsageCounter struct {
	bun.BaseModel `bun:"table:org_usage_counters"`

	OrganizationUID string `bun:"organization_uid,pk"`
	Kind            string `bun:"kind,pk"`
	PeriodStart     string `bun:"period_start,pk"`
	Count           int    `bun:"count,notnull"`
}

OrgUsageCounter is a persistent per-org, per-kind, per-period counter backing the SMS/voice/WhatsApp monthly quotas and the daily rate-limited-skip tally. PeriodStart is an ISO date string ("2006-01-02") whose granularity is decided by the kind — the first day of the UTC month for the monthly quotas, the UTC day itself for UsageCounterKindCheckRateLimited. It is stored in a date column on PostgreSQL and text on SQLite.

type Organization

type Organization struct {
	UID  string `bun:"uid,pk,type:varchar(36)"`
	Slug string `bun:"slug,notnull"`
	Name string `bun:"name"`
	// DefaultEscalationPolicyUID is the org-wide fallback escalation policy for
	// checks that resolve to no policy of their own (check → group → org default
	// → none). NULL = no org default (legacy behavior). FK `on delete set null`.
	DefaultEscalationPolicyUID *string `bun:"default_escalation_policy_uid"`
	// LogoURL is the organization's logo: either an external http(s) URL, or
	// "/pub/assets/<file uid>" for a logo uploaded through the files
	// subsystem. NULL means "no logo", and every surface falls back to the
	// product default.
	LogoURL *string `bun:"logo_url"`
	// LogoFileUID points at the uploaded file backing LogoURL. It is NULL for
	// external-URL logos. Replacing an uploaded logo retires the previous blob
	// through it. It is NO LONGER an authorization input: since spec
	// 2026-08-22-03 the unsigned public route reads the FILE's own attachment
	// topic (`organizations/<uid>/logo`) and serves live rows only.
	LogoFileUID *string    `bun:"logo_file_uid"`
	CreatedAt   time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt   time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt   *time.Time `bun:"deleted_at"`
}

Organization represents a tenant in the system.

func NewOrganization

func NewOrganization(slug, name string) *Organization

NewOrganization creates a new organization with generated UID.

type OrganizationMember

type OrganizationMember struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	UserUID         string     `bun:"user_uid,notnull"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	Role            MemberRole `bun:"role,notnull"`
	InvitedByUID    *string    `bun:"invited_by_uid"`
	InvitedAt       *time.Time `bun:"invited_at"`
	JoinedAt        *time.Time `bun:"joined_at"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`

	// Relations (for eager loading)
	User         *User         `bun:"rel:belongs-to,join:user_uid=uid"`
	Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
	InvitedBy    *User         `bun:"rel:belongs-to,join:invited_by_uid=uid"`
}

OrganizationMember links a user to an organization with a role.

func NewOrganizationMember

func NewOrganizationMember(orgUID, userUID string, role MemberRole) *OrganizationMember

NewOrganizationMember creates a new membership with generated UID.

type OrganizationMemberUpdate

type OrganizationMemberUpdate struct {
	Role     *MemberRole
	JoinedAt *time.Time
}

OrganizationMemberUpdate represents fields that can be updated.

type OrganizationPreviousSlug

type OrganizationPreviousSlug struct {
	bun.BaseModel `bun:"table:organization_previous_slugs,alias:org_previous_slug"`

	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	Slug            string     `bun:"slug,notnull"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

OrganizationPreviousSlug is a slug an organization used to answer on before it was renamed. Resolution always prefers a live organizations.slug, so an alias can never shadow an existing org; on a miss the alias resolves and the request is redirected to the organization's current slug.

The alias is released (soft-deleted) as soon as another organization claims the slug, which is the only guarantee boundary the UI promises. Deleted orgs are deliberately unreachable through aliases (spec 2026-08-08-11): the lookup joins organizations and requires deleted_at IS NULL.

func NewOrganizationPreviousSlug

func NewOrganizationPreviousSlug(orgUID, slug string) *OrganizationPreviousSlug

NewOrganizationPreviousSlug creates an alias row with a generated UID.

type OrganizationProvider

type OrganizationProvider struct {
	UID             string       `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string       `bun:"organization_uid,notnull"`
	ProviderType    ProviderType `bun:"provider_type,notnull"`
	ProviderID      string       `bun:"provider_id,notnull"` // e.g., Slack Team ID T0123456789
	ProviderName    string       `bun:"provider_name"`       // e.g., "Acme Corp Slack Workspace"
	Metadata        JSONMap      `bun:"metadata,type:jsonb,nullzero"`
	// MetadataPrivate / MetadataPrivateKeys mirror the credential-encryption
	// shape used on Check.Config — OAuth client secrets and similar live
	// here as an AES-GCM envelope at rest.
	MetadataPrivate     *string    `bun:"metadata_private,type:text,nullzero"`
	MetadataPrivateKeys *string    `bun:"metadata_private_keys,type:text,nullzero"`
	CreatedAt           time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt           time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt           *time.Time `bun:"deleted_at"`

	// Relations (for eager loading)
	Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}

OrganizationProvider links an organization to an external provider identity. This is the single source of truth for org↔provider mapping (e.g., Slack team, Google Workspace).

func NewOrganizationProvider

func NewOrganizationProvider(orgUID string, providerType ProviderType, providerID string) *OrganizationProvider

NewOrganizationProvider creates a new organization provider with generated UID.

type OrganizationProviderUpdate

type OrganizationProviderUpdate struct {
	ProviderName         *string
	Metadata             *JSONMap
	MetadataPrivate      *string
	MetadataPrivateKeys  *string
	ClearMetadataPrivate bool
}

OrganizationProviderUpdate represents fields that can be updated.

type OrganizationUpdate

type OrganizationUpdate struct {
	Slug *string
	Name *string
	// DefaultEscalationPolicyUID sets the org-wide default escalation policy.
	// ClearDefaultEscalationPolicyUID takes precedence and clears it to NULL
	// (mirrors the check-group clear-flag pattern).
	DefaultEscalationPolicyUID      *string
	ClearDefaultEscalationPolicyUID bool
	// LogoURL / LogoFileUID follow the same set-or-clear pattern: the Clear*
	// flag wins and writes NULL, a non-nil pointer writes the value, and nil
	// with no flag leaves the column untouched (PATCH semantics).
	LogoURL          *string
	ClearLogoURL     bool
	LogoFileUID      *string
	ClearLogoFileUID bool
}

OrganizationUpdate represents fields that can be updated.

type PageResourceStatus

type PageResourceStatus struct {
	Status        CheckStatus
	InMaintenance bool
}

PageResourceStatus is the minimal per-resource input RollupPageStatus needs: the resource's live check status (a check's own CheckStatus, or a check-group resource's already rolled-up status via RollupGroupStatus) and whether it is currently inside an active maintenance window.

type PageStatus

type PageStatus string

PageStatus is a status page's page-level rollup status (spec 2026-08-08-05). It is a distinct vocabulary from CheckStatus: it adds "maintenance" (no individual check has this concept) and gives "unknown" a real, honest meaning instead of silently collapsing into "operational".

const (
	PageStatusOperational PageStatus = "operational"
	PageStatusDegraded    PageStatus = "degraded"
	PageStatusDown        PageStatus = "down"
	PageStatusMaintenance PageStatus = "maintenance"
	PageStatusUnknown     PageStatus = "unknown"
)

The page-level rollup vocabulary. Kept as string constants (rather than an int enum like CheckStatus) because this value is only ever produced at the read boundary and serialized straight onto the public wire — there is no storage representation to keep compact.

type PageStatusCounts

type PageStatusCounts struct {
	Operational int
	Degraded    int
	Down        int
	Maintenance int
	Unknown     int
}

PageStatusCounts tallies how many resources landed in each PageStatus category. Per-resource classification applies the same maintenance-masking rule as the overall rollup: a resource in maintenance is counted as Maintenance regardless of its underlying check status.

type Parameter

type Parameter struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID *string    `bun:"organization_uid"`
	Key             string     `bun:"key,notnull"`
	Value           JSONMap    `bun:"value,type:jsonb,notnull"`
	Secret          *bool      `bun:"secret"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

Parameter represents a key-value configuration. When OrganizationUID is nil, this is a system-wide parameter.

func NewParameter

func NewParameter(orgUID, key string, value JSONMap) *Parameter

NewParameter creates a new organization-scoped parameter with generated UID.

func NewSystemParameter

func NewSystemParameter(key string, value JSONMap, secret bool) *Parameter

NewSystemParameter creates a new system-wide parameter (organization_uid = nil).

type ParameterUpdate

type ParameterUpdate struct {
	Key    *string
	Value  *JSONMap
	Secret *bool
}

ParameterUpdate represents fields that can be updated.

type PeriodTierSide

type PeriodTierSide int

PeriodTierSide describes which side of the raw/rollup split a requested set of period types sits on. It exists because BOTH useful indexes on `results` are partial and split on exactly that predicate (`results_raw_idx WHERE period_type = 'raw'`, `results_aggregated_idx WHERE period_type != 'raw'`): a query can only ride one of them if its WHERE clause implies the index's own predicate. See PeriodTypesTierSide.

const (
	// PeriodTierMixed names both raw and at least one rollup tier, so neither
	// partial index is eligible and the query can only be a full scan. Never
	// issue one — split it into one query per side (spec 2026-08-22-04).
	PeriodTierMixed PeriodTierSide = iota
	// PeriodTierRaw names only raw.
	PeriodTierRaw
	// PeriodTierRollup names only aggregated tiers (hour/day/month).
	PeriodTierRollup
)

func PeriodTypesTierSide

func PeriodTypesTierSide(periodTypes []string) PeriodTierSide

PeriodTypesTierSide reports which side of the raw/rollup index split the given period types sit on. An empty list is mixed: it constrains nothing, so neither partial predicate is implied.

type ProviderType

type ProviderType string

ProviderType represents an external auth provider type.

const (
	ProviderTypeGoogle    ProviderType = "google"
	ProviderTypeGitHub    ProviderType = "github"
	ProviderTypeGitLab    ProviderType = "gitlab"
	ProviderTypeMicrosoft ProviderType = "microsoft"
	ProviderTypeTwitter   ProviderType = "twitter"
	ProviderTypeSlack     ProviderType = "slack"
	ProviderTypeDiscord   ProviderType = "discord"
	ProviderTypeSAML      ProviderType = "saml"
	ProviderTypeOIDC      ProviderType = "oidc"
	// ProviderTypeLDAP identifies a user auto-provisioned or linked via an
	// LDAP/Active Directory bind (spec 2026-07-08-08, part 3). Users linked
	// this way always have a nil User.PasswordHash — see
	// Service.findOrCreateLDAPUser in internal/handlers/auth/ldap_service.go.
	ProviderTypeLDAP ProviderType = "ldap"
)

Provider types.

type PublicationSeverity

type PublicationSeverity string

PublicationSeverity is the public badge severity. It is display-only: it never routes, pages, or gates anything. Empty/NULL means "no badge".

const (
	// PublicationSeverityMinor is a limited-impact issue.
	PublicationSeverityMinor PublicationSeverity = "minor"
	// PublicationSeverityMajor is a broad-impact issue.
	PublicationSeverityMajor PublicationSeverity = "major"
	// PublicationSeverityCritical is a full outage.
	PublicationSeverityCritical PublicationSeverity = "critical"
)

PublicationSeverity values.

func (PublicationSeverity) IsValid

func (s PublicationSeverity) IsValid() bool

IsValid reports whether the severity is one of the three recognized values.

type PublicationState

type PublicationState string

PublicationState is the customer-facing lifecycle state of an incident publication. It is deliberately a SEPARATE vocabulary from models.IncidentState: the operational incident is active/resolved, while the public narrative walks investigating → identified → monitoring → resolved (the StatusUpdateKind vocabulary customers already see on the timeline).

const (
	// PublicationStateInvestigating is the opening state of every publication.
	PublicationStateInvestigating PublicationState = "investigating"
	// PublicationStateIdentified means the cause is known, work is ongoing.
	PublicationStateIdentified PublicationState = "identified"
	// PublicationStateMonitoring means a fix is in place and being watched.
	PublicationStateMonitoring PublicationState = "monitoring"
	// PublicationStateResolved closes the publication.
	PublicationStateResolved PublicationState = "resolved"
)

PublicationState values.

func (PublicationState) IsValid

func (s PublicationState) IsValid() bool

IsValid reports whether the state is one of the four recognized values.

func (PublicationState) UpdateKind

func (s PublicationState) UpdateKind() StatusUpdateKind

UpdateKind maps a publication state onto the StatusUpdateKind used for the narrative row posted alongside a state change. The two vocabularies are intentionally identical in wording so a reader never sees the timeline and the incident header disagree.

type ReapAbandonedResultsOutcome

type ReapAbandonedResultsOutcome struct {
	// Candidates is the number of raw rows found sitting in ResultStatusCreated
	// (see abandonedResultLifecycleStatuses), before threshold filtering.
	Candidates int
	// Reaped is the number of those candidates that were past
	// AbandonedResultThreshold for their check and got finalized this sweep.
	Reaped int
}

ReapAbandonedResultsOutcome reports what one abandoned-result reaper sweep did (see db.Service.ReapAbandonedResults).

type RecentResultsPerCheckFilter

type RecentResultsPerCheckFilter struct {
	// OrganizationUID scopes every branch. Required.
	OrganizationUID string
	// CheckUIDs are the checks to fetch for. An empty list yields no rows.
	CheckUIDs []string
	// Tiers are the tier-aligned branches, UNION ALLed together. Required, and
	// each one must be single-sided.
	Tiers []RecentResultsTier
	// PerCheckLimits is the per-check row budget, per branch. A check absent
	// from the map uses DefaultPerCheckLimit.
	PerCheckLimits map[string]int
	// DefaultPerCheckLimit is the budget for a check with no explicit entry.
	// Required (> 0) — an unbounded branch defeats the whole point.
	DefaultPerCheckLimit int
}

RecentResultsPerCheckFilter describes a "newest N rows per check, per tier" fetch. It is deliberately NOT expressible as a ListResultsFilter: that is a generic filtered list whose Limit is global, and a global limit over several checks is exactly the over-fetch-and-discard this query shape exists to remove (spec 2026-08-22-05).

The budget is per check rather than one number for the batch because the caller sizes it from the check's own region fan-out: a 3-region check needs three times the rows of a single-region one to fill the same per-region chart, and a global figure can only be the max of those — which over-fetches for every other check on the page.

func (*RecentResultsPerCheckFilter) LimitFor

func (f *RecentResultsPerCheckFilter) LimitFor(checkUID string) int

LimitFor returns the per-branch row budget for one check.

func (*RecentResultsPerCheckFilter) Validate

func (f *RecentResultsPerCheckFilter) Validate() error

Validate rejects a filter no dialect may execute. The mixed-tier rule is the important one: see ErrRecentResultsMixedTier.

type RecentResultsTier

type RecentResultsTier struct {
	// PeriodTypes are the tiers this branch reads. They MUST all sit on one
	// side of the raw/rollup split (see PeriodTypesTierSide) — the
	// implementation restates that side as an explicit predicate so both
	// Postgres and SQLite can use the matching partial index.
	PeriodTypes []string
	// Since is this branch's inclusive period_start lower bound.
	Since time.Time
}

RecentResultsTier is ONE tier-aligned branch of a RecentResultsPerCheck fetch: which aggregation tiers to read, and how far back to read them.

Each branch gets its own lower bound because the tiers live in different windows: raw only exists for the configured raw retention (~24 h), while the rollups reach back months. One shared bound would either truncate the rollups or make the raw branch scan far past where raw rows can exist.

type ReportSchedule

type ReportSchedule struct {
	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull"`
	Name            string `bun:"name,notnull"`
	Frequency       string `bun:"frequency,notnull"`
	Timezone        string `bun:"timezone,notnull"`
	// Recipients is PII, held to the same bar as status-page subscriber
	// addresses: never logged, never emitted into events, only ever read back
	// to the org's own admins.
	Recipients []string `bun:"recipients,type:jsonb,nullzero"`
	// CheckUIDs / CheckGroupUIDs scope the digest. Both empty means org-wide.
	CheckUIDs      []string `bun:"check_uids,type:jsonb,nullzero"`
	CheckGroupUIDs []string `bun:"check_group_uids,type:jsonb,nullzero"`
	IncludeSLOs    bool     `bun:"include_slos,notnull"`
	Enabled        bool     `bun:"enabled,notnull"`
	// LastPeriodStart is the UTC start of the last period actually reported.
	// It is the duplicate-run suppression key: a second job fire for the same
	// closed period is a no-op, which is what makes multi-replica scheduling
	// safe without leader election.
	LastPeriodStart *time.Time `bun:"last_period_start"`
	LastRunAt       *time.Time `bun:"last_run_at"`
	CreatedAt       time.Time  `bun:"created_at,notnull"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

ReportSchedule is a recurring uptime-report digest.

It is deliberately NOT a column on SLO: a digest is useful for checks that carry no formal objective at all, and an SLO is useful without anybody being emailed about it.

func NewReportSchedule

func NewReportSchedule(orgUID, name, frequency string) *ReportSchedule

NewReportSchedule builds a report schedule with a generated UID and defaults.

func (*ReportSchedule) IsOrgWide

func (r *ReportSchedule) IsOrgWide() bool

IsOrgWide reports whether the schedule covers every check in the org.

type ReportScheduleUpdate

type ReportScheduleUpdate struct {
	Name           *string
	Frequency      *string
	Timezone       *string
	Recipients     *[]string
	CheckUIDs      *[]string
	CheckGroupUIDs *[]string
	IncludeSLOs    *bool
	Enabled        *bool
}

ReportScheduleUpdate carries the fields a PATCH may change. Nil = leave alone.

type Result

type Result struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	CheckUID        string     `bun:"check_uid,notnull"`
	PeriodType      string     `bun:"period_type,notnull"`
	PeriodStart     time.Time  `bun:"period_start,notnull"`
	PeriodEnd       *time.Time `bun:"period_end"`
	Region          *string    `bun:"region"`

	// Raw result fields (period_type = 'raw')
	WorkerUID *string  `bun:"worker_uid"`
	Status    *int     `bun:"status"`
	Duration  *float32 `bun:"duration"`
	Metrics   JSONMap  `bun:"metrics,type:jsonb,nullzero"`
	Output    JSONMap  `bun:"output,type:jsonb,nullzero"`

	// Maintenance records that an active maintenance window covered this
	// check at the moment the probe was recorded (spec 2026-08-20-01). It is
	// set at ingest and never backfilled: rollup buckets cannot be sliced
	// after the fact, so the tag has to exist before aggregation runs. Raw
	// rows only.
	//
	// It changes NO existing availability number. Status pages, badges and the
	// availability API keep counting maintenance probes exactly as before;
	// only an SLO with ExcludeMaintenance set subtracts them.
	Maintenance bool `bun:"maintenance,notnull"`

	// Diagnostics is the opt-in capture of what the probe saw (spec
	// 2026-08-20-01). It is TRANSIENT by construction: `bun:"-"` keeps it out
	// of every INSERT/UPDATE/SELECT so it can never reach the `output` JSONB
	// column or any other column, and `json:"-"` keeps it off every API
	// response built from this struct. It exists on the model only so the
	// result-submission path can hand it to the incident pipeline, which is
	// the ONLY thing allowed to persist it (onto incidents.details, and only
	// on an incident open/reopen).
	Diagnostics *checkerdef.Diagnostics `bun:"-" json:"-"`

	// Aggregated fields (period_type = 'hour', 'day', 'month', 'year').
	// availability_pct is intentionally absent: it is derived at read time from
	// successful_checks / total_checks rather than stored (spec 2026-07-24-02).
	TotalChecks      *int `bun:"total_checks"`
	SuccessfulChecks *int `bun:"successful_checks"`
	// MaintenanceChecks / MaintenanceSuccessfulChecks are SUBSETS of the two
	// counters above: how many of the probes folded into this bucket were
	// recorded while an active maintenance window covered the check
	// (spec 2026-08-20-01).
	//
	// They are additive across tiers exactly like their parents, and the tiers
	// are disjoint by construction (the aggregation job compacts and deletes in
	// one transaction), so a month row's counters are the exact count of raw
	// probes it descends from — no double counting.
	//
	// Being subsets rather than replacements is what keeps this change inert
	// for every existing consumer: availability stays successful/total.
	MaintenanceChecks           *int     `bun:"maintenance_checks"`
	MaintenanceSuccessfulChecks *int     `bun:"maintenance_successful_checks"`
	DurationMin                 *float32 `bun:"duration_min"`
	DurationMax                 *float32 `bun:"duration_max"`
	DurationP95                 *float32 `bun:"duration_p95"`
	DurationAvg                 *float32 `bun:"duration_avg"`

	CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`

	// CheckSlug and CheckName are populated only when the query joined
	// `checks` (ListResultsFilter.IncludeCheckInfo, set when the results
	// endpoint's caller requests with=checkSlug,checkName). Scan-only and
	// transient — never selected, inserted, or updated outside that query;
	// bun scans the joined `check_slug`/`check_name` aliases straight into
	// these tags without a real relation. A LEFT JOIN leaves both nil for a
	// result whose check has since been hard-deleted (results can outlive a
	// hard-deleted check, see ListResultsFilter.RequireCheckExists) rather
	// than dropping the row from the page.
	CheckSlug *string `bun:"check_slug,scanonly"`
	CheckName *string `bun:"check_name,scanonly"`
}

Result represents a check execution result.

func NewResult

func NewResult(orgUID, checkUID string, status ResultStatus, duration float32) *Result

NewResult creates a new raw result with generated UID.

func (*Result) ExcludedFromAvailability

func (r *Result) ExcludedFromAvailability() bool

ExcludedFromAvailability reports whether a raw result must be dropped from both availability's numerator and denominator: a lifecycle marker (still in flight — created/running) OR a row the abandoned-result reaper finalized (ResultStatusAbandoned — terminal, but evidence of OUR infrastructure failing, not the monitored service — spec 2026-08-18-03). RawAvailability, the hour rollup's processRawResult (job_aggregation.go), and uptimebar's accumulateRaw (uptimebar/bucketing.go) all route through this one predicate so the three surfaces can never drift apart on what counts.

A row with no status at all is NOT excluded: that is an aggregated rollup row, which never reaches this predicate's raw-only callers, and treating a missing status as "skip" would silently swallow a malformed raw row instead of counting it.

type ResultStatus

type ResultStatus int

ResultStatus represents the status of a check result.

const (
	// ResultStatusCreated indicates the check was just created and hasn't been executed yet.
	ResultStatusCreated ResultStatus = 1
	// ResultStatusRunning indicates the check process has started but not yet completed.
	ResultStatusRunning ResultStatus = 2
	// ResultStatusUp indicates the check passed successfully.
	ResultStatusUp ResultStatus = 3
	// ResultStatusDown indicates the check failed.
	ResultStatusDown ResultStatus = 4
	// ResultStatusTimeout indicates the check timed out.
	ResultStatusTimeout ResultStatus = 5
	// ResultStatusError indicates the check encountered an error.
	ResultStatusError ResultStatus = 6
	// ResultStatusDegraded is the aggregated rollup status: a window contained
	// warning(s) but no dominating failure. Stored only on aggregated rows
	// (period_type = hour/day/month) by the aggregation job, never on raw rows.
	ResultStatusDegraded ResultStatus = 7
	// ResultStatusWarning indicates the target is up but there is something to
	// report. Stored on raw rows; counts as up for availability.
	ResultStatusWarning ResultStatus = 8
	// ResultStatusAbandoned marks a raw attempt the abandoned-result reaper
	// finalized from a stale `created` marker (spec 2026-08-18-03): nothing was
	// ever reported for it because OUR side died — a worker crash, a devloop
	// restart, a lost lease — not because the monitored service was down. It is
	// terminal (the timeline keeps honest evidence an attempt happened) but is
	// excluded from availability math everywhere, unlike a genuine
	// ResultStatusError; see ExcludedFromAvailability.
	//
	// Server-minted ONLY, by ReapAbandonedResults. A worker or a deported agent
	// can never report it: handlers/agentws validates inbound statuses to
	// [ResultStatusCreated..ResultStatusError] and so rejects 9 outright.
	ResultStatusAbandoned ResultStatus = 9
)

func (ResultStatus) CountsAsUp

func (s ResultStatus) CountsAsUp() bool

CountsAsUp reports whether a raw result counts toward availability success. Warning is "up with something to report" (the target is reachable), so it counts.

func (ResultStatus) ExcludedFromAvailability

func (s ResultStatus) ExcludedFromAvailability() bool

ExcludedFromAvailability reports whether a raw status must be dropped from both availability's numerator and denominator: a lifecycle marker (still in flight — created/running) OR ResultStatusAbandoned (terminal, but evidence of OUR infrastructure failing, not the monitored service — spec 2026-08-18-03). This is the one place the rule lives; (*Result) delegates to it so a caller holding only a status and a caller holding a whole row can never disagree.

func (ResultStatus) IsLifecycleMarker

func (s ResultStatus) IsLifecycleMarker() bool

IsLifecycleMarker reports whether the status is a non-measurement lifecycle state (created/running) that must be excluded from availability denominators.

type RotationType

type RotationType string

RotationType is the cadence at which a schedule rotates between users.

const (
	// RotationTypeDaily rotates every day at handoff_time.
	RotationTypeDaily RotationType = "daily"
	// RotationTypeWeekly rotates once a week, at handoff_time on handoff_weekday.
	RotationTypeWeekly RotationType = "weekly"
)

type SLO

type SLO struct {
	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull"`
	Name            string `bun:"name,notnull"`
	Slug            string `bun:"slug,notnull"`
	// Exactly one of CheckUID / CheckGroupUID is set — enforced by the
	// slos_scope_xor CHECK constraint, not merely by convention.
	CheckUID      *string `bun:"check_uid"`
	CheckGroupUID *string `bun:"check_group_uid"`
	// TargetPct is the objective, e.g. 99.9. 0 < TargetPct <= 100.
	TargetPct float64 `bun:"target_pct,notnull"`
	// Timezone is the IANA zone the calendar month is resolved in.
	Timezone string `bun:"timezone,notnull"`
	// ExcludeMaintenance subtracts probes tagged results.maintenance from this
	// SLO's denominator. It affects nothing outside the SLO read path.
	ExcludeMaintenance bool       `bun:"exclude_maintenance,notnull"`
	Enabled            bool       `bun:"enabled,notnull"`
	CreatedAt          time.Time  `bun:"created_at,notnull"`
	UpdatedAt          time.Time  `bun:"updated_at,notnull"`
	DeletedAt          *time.Time `bun:"deleted_at"`
}

SLO represents a service-level objective over one check or one check group.

Windows are calendar months in Timezone and nothing per-window is stored: attainment, error budget and history are always recomputed at read time off the permanent `month` rollups. The emailed uptime report is the only frozen artifact.

func NewSLO

func NewSLO(orgUID, name, slug string, targetPct float64) *SLO

NewSLO builds an SLO with a generated UID and the default scalars. The caller sets exactly one of CheckUID / CheckGroupUID.

type SLOAlertPolicy

type SLOAlertPolicy struct {
	// Named explicitly rather than left to the inflector: "SLOAlertPolicy" is
	// exactly the acronym-prefixed shape struct-name pluralization gets wrong.
	bun.BaseModel `bun:"table:slo_alert_policies,alias:slo_alert_policies"`

	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull"`
	SLOUID          string `bun:"slo_uid,notnull"`
	// Kind is the built-in identity, unique per SLO.
	Kind string `bun:"kind,notnull"`
	// Enabled defaults to false: upgrading to a version that has alerting must
	// never start paging on its own.
	Enabled bool `bun:"enabled,notnull"`
	// LongWindowSeconds / ShortWindowSeconds bound the two rolling windows the
	// evaluator measures, both ending at "now".
	LongWindowSeconds  int `bun:"long_window_seconds,notnull"`
	ShortWindowSeconds int `bun:"short_window_seconds,notnull"`
	// Threshold is the burn-rate multiple both windows must exceed. 1.0 spends
	// the calendar budget exactly by period end; 14.4 spends a 30-day budget in
	// about two hours.
	Threshold float64 `bun:"threshold,notnull"`
	Severity  string  `bun:"severity,notnull"`
	// MinSamples is the per-window probe floor. Below it the window is
	// INCONCLUSIVE: it does not fire, and it equally does not count as "below
	// threshold" for the resolution hysteresis.
	MinSamples int `bun:"min_samples,notnull"`
	// LastEvaluatedAt / LastLongBurnRate / LastShortBurnRate are the live
	// readout the dashboard's Alerting section renders. Stored so the UI and
	// the evaluator cannot disagree about what the burn rate was a minute ago.
	LastEvaluatedAt   *time.Time `bun:"last_evaluated_at"`
	LastLongBurnRate  *float64   `bun:"last_long_burn_rate"`
	LastShortBurnRate *float64   `bun:"last_short_burn_rate"`
	// BelowThresholdSince is the hysteresis anchor: the instant BOTH windows
	// first dropped below Threshold. Resolution waits until that has held for a
	// full short window; it is cleared the moment either window goes back over,
	// so a flapping burn never resolves.
	BelowThresholdSince *time.Time `bun:"below_threshold_since"`
	CreatedAt           time.Time  `bun:"created_at,notnull"`
	UpdatedAt           time.Time  `bun:"updated_at,notnull"`
}

SLOAlertPolicy is one built-in multiwindow burn-rate policy attached to one SLO.

Google-SRE-style multiwindow alerting: the LONG window proves the burn is significant, the SHORT window proves it is still happening. An alert fires only when both exceed Threshold, which is precisely what stops a spike that ended forty minutes ago from paging for the rest of the hour.

Thresholds and windows are columns rather than constants because the SRE-workbook numbers are a starting point, not a law: an org whose traffic shape makes 14.4x twitchy has to be able to retune it without a deploy.

func NewSLOAlertPolicy

func NewSLOAlertPolicy(orgUID, sloUID string, def *SLOAlertPolicyDefault) *SLOAlertPolicy

NewSLOAlertPolicy builds a policy row from a built-in default.

func (*SLOAlertPolicy) LongWindow

func (p *SLOAlertPolicy) LongWindow() time.Duration

LongWindow returns the long window as a duration.

func (*SLOAlertPolicy) ShortWindow

func (p *SLOAlertPolicy) ShortWindow() time.Duration

ShortWindow returns the short (confirmation) window as a duration.

type SLOAlertPolicyDefault

type SLOAlertPolicyDefault struct {
	Kind               string
	LongWindowSeconds  int
	ShortWindowSeconds int
	Threshold          float64
	Severity           string
}

SLOAlertPolicyDefault describes one built-in policy's shipped configuration.

func DefaultSLOAlertPolicies

func DefaultSLOAlertPolicies() []SLOAlertPolicyDefault

DefaultSLOAlertPolicies is the shipped pair, following the SRE-workbook 99.9% table: fast = 1h/5m at 14.4x (2% of a 30-day budget per hour), slow = 6h/30m at 6x.

type SLOAlertPolicyUpdate

type SLOAlertPolicyUpdate struct {
	Enabled            *bool
	LongWindowSeconds  *int
	ShortWindowSeconds *int
	Threshold          *float64
	Severity           *string
	MinSamples         *int

	LastEvaluatedAt   *time.Time
	LastLongBurnRate  *float64
	LastShortBurnRate *float64

	BelowThresholdSince *time.Time
	// ClearBelowThresholdSince sets the hysteresis anchor back to NULL — used
	// the instant either window climbs back over the threshold.
	ClearBelowThresholdSince bool
	// ClearLastBurnRates nulls the live readout when a window turns
	// inconclusive, so the UI shows "no data" rather than a stale number.
	ClearLastBurnRates bool
}

SLOAlertPolicyUpdate carries the fields a PATCH may change plus the evaluator's state writes. Nil means "leave alone".

type SLOUpdate

type SLOUpdate struct {
	Name               *string
	Slug               *string
	CheckUID           *string
	CheckGroupUID      *string
	TargetPct          *float64
	Timezone           *string
	ExcludeMaintenance *bool
	Enabled            *bool
}

SLOUpdate carries the fields a PATCH may change. Nil means "leave alone".

type SectionSelector

type SectionSelector struct {
	// All selects every check in the organization. Internal checks are
	// excluded (see Filter) — auto-publishing an internal probe onto a public
	// page is precisely the disclosure footgun this feature has to avoid.
	All bool `json:"all,omitempty"`
	// Labels selects checks carrying ALL of these exact key=value labels.
	Labels map[string]string `json:"labels,omitempty"`
}

SectionSelector is the dynamic-membership rule of a status page section (spec 2026-08-29-11). A section with a selector has its check resources MATERIALIZED by the reconciler rather than hand-curated: real StatusPageResource rows flagged ManagedBySelector, so every downstream consumer (availability enrichment, positions, badge/summary/embed, publications' affectedResources) keeps working unchanged.

Exactly one of the two shapes is legal:

{"all": true}                              — every non-internal check in the org
{"labels": {"env": "prod", "public": "true"}} — AND over exact key=value pairs

Values are exact in v1: there is no existence-only ("*") matching. `all` and `labels` are mutually exclusive, and an empty `labels` object is rejected rather than quietly meaning `all` — "select everything" must be typed out.

func (*SectionSelector) Equal

func (sel *SectionSelector) Equal(other *SectionSelector) bool

Equal reports whether two selectors describe the same membership rule. Used to decide whether a section update actually changed anything.

func (*SectionSelector) Filter

func (sel *SectionSelector) Filter() *ListChecksFilter

Filter renders the selector as a ListChecksFilter, so selector matching is literally the same query the checks list uses — there is no second matching implementation to drift.

Internal is left nil on purpose, which ListChecks reads as `internal = FALSE`. Internal checks are the org's own plumbing probes; a selector must never sweep one onto a status page.

func (*SectionSelector) Validate

func (sel *SectionSelector) Validate() error

Validate reports whether the selector is a legal v1 selector.

type Severity

type Severity struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull"`
	Slug            string     `bun:"slug,notnull"`
	Name            string     `bun:"name,notnull"`
	Description     *string    `bun:"description"`
	Channels        string     `bun:"channels,notnull"`
	IsDefault       bool       `bun:"is_default,notnull"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

Severity is the per-org channel-set primitive for escalation steps. Spec 2026-05-08-03 introduces it so a step can say "page critically" and fan out to email + sms + voice in a single tick rather than a chain of single-channel steps.

Channels is stored as a JSON-encoded array of channel-type strings: "email", "slack", "discord", … from ConnectionType, plus the synthetic direct-channel types "sms"/"voice"/"push"/"critical_push" that gate behind future provider integrations.

func NewSeverity

func NewSeverity(orgUID, slug, name string, channels []string, isDefault bool) *Severity

NewSeverity creates a Severity with a generated UID and the given channel-type list serialized to its `channels` column.

func (*Severity) ChannelList

func (s *Severity) ChannelList() []string

ChannelList returns the parsed channel-type list from the JSON-encoded Channels column. Returns an empty slice on parse error so callers can treat the row as "fan out to nothing" rather than crashing.

type SeveritySeed

type SeveritySeed struct {
	Slug      string
	Name      string
	Channels  []string
	IsDefault bool
}

SeveritySeed describes one of the per-org default severities seeded when an organization is first created. Kept in models so both the migrate path and the org-creation hook can share the source of truth.

func DefaultSeveritySeeds

func DefaultSeveritySeeds() []SeveritySeed

DefaultSeveritySeeds returns the three rows seeded for every new org. The "default" row is the org's default severity used by escalation steps that omit `severityUid` and target a user / all_admins.

type SeverityUpdate

type SeverityUpdate struct {
	Slug         *string
	Name         *string
	Description  *string
	Channels     *[]string
	IsDefault    *bool
	ClearDefault bool
}

SeverityUpdate represents fields that can be updated on a severity. Pointer-typed fields use nil to mean "leave alone"; channel slices use a separate flag because []string{} is itself a meaningful value ("page nothing").

type SignupAttribution

type SignupAttribution struct {
	Source   string `json:"utmSource,omitempty"`
	Medium   string `json:"utmMedium,omitempty"`
	Campaign string `json:"utmCampaign,omitempty"`
	Term     string `json:"utmTerm,omitempty"`
	Content  string `json:"utmContent,omitempty"`
	// ClickIDKind names the query parameter the click id arrived in — gclid,
	// gbraid, wbraid (Google) or msclkid (Microsoft) — so the upload goes to
	// the right network.
	ClickIDKind string `json:"clickIdKind,omitempty"`
	ClickID     string `json:"clickId,omitempty"`
	// LandingPath is the dashboard path the tagged link opened, without its
	// query string. Useful for telling an ad landing from a docs deep link.
	LandingPath string `json:"landingPath,omitempty"`
	// CapturedAt is when the dashboard first saw the tags, which can be days
	// before the account exists (the confirmation email is not always opened
	// at once).
	CapturedAt *time.Time `json:"capturedAt,omitempty"`
}

SignupAttribution is the campaign context of a signup, as forwarded by the marketing site (www.solidping.io appends it to every link into the dashboard). Every field is optional and every value is an opaque string chosen by whoever wrote the ad campaign — none of it identifies the person.

The click identifier is the one field with a purpose beyond reporting: an offline conversion upload to the ad network needs the click id and the time the account was created, and nothing else. That is the whole reason this is persisted rather than only counted in analytics.

func (*SignupAttribution) IsEmpty

func (a *SignupAttribution) IsEmpty() bool

IsEmpty reports whether nothing at all was captured, so callers can store a nil pointer instead of an empty object.

type SlackSettings

type SlackSettings struct {
	TeamID            string   `json:"team_id"`
	TeamName          string   `json:"team_name"`
	BotUserID         string   `json:"bot_user_id"`
	AccessToken       string   `json:"access_token"`
	ChannelID         string   `json:"channel_id,omitempty"`
	ChannelName       string   `json:"channel_name,omitempty"`
	DestinationType   string   `json:"destination_type,omitempty"` // "channel" | "dm" | ""
	DisplayName       string   `json:"display_name,omitempty"`     // "#alerts" or "@alice"
	InstalledByUserID string   `json:"installed_by_user_id"`
	Scopes            []string `json:"scopes"`
	// MentionOnCall makes channel alerts ping the humans the escalation policy
	// would page first (`<@U123ABC>`), so a channel message tells the
	// responsible person it is theirs.
	//
	// Zero value is deliberately false: every integration stored before this
	// field existed has no such key in its settings JSON, decodes to false, and
	// keeps behaving exactly as before — no backfill, no migration. New Slack
	// integrations get `true` written explicitly by the install flow (see
	// slack.Service.createOrUpdateConnection).
	MentionOnCall bool `json:"mention_on_call"`
	// CommentIngestion selects how inbound Slack thread replies are treated:
	//
	//   - SlackCommentIngestionExplicit (default, and the meaning of an empty
	//     value): only an explicit `/comment` becomes an incident comment.
	//     Triage chatter in the thread — "lunch?", "who's on call?" — stays
	//     chatter instead of becoming permanent incident-timeline content.
	//   - SlackCommentIngestionAll: every human thread reply is ingested, the
	//     historical behavior, kept for teams that want it.
	//
	// Zero value is deliberately the safe one: an integration stored before
	// this field existed decodes to "" and therefore stops over-capturing,
	// which is the whole point of the change.
	CommentIngestion string `json:"comment_ingestion,omitempty"`
}

SlackSettings represents Slack-specific settings stored in the Settings JSONB.

func SlackSettingsFromJSONMap

func SlackSettingsFromJSONMap(m JSONMap) (*SlackSettings, error)

SlackSettingsFromJSONMap parses SlackSettings from a JSONMap.

func (*SlackSettings) DMCaptureAvailable

func (s *SlackSettings) DMCaptureAvailable() bool

DMCaptureAvailable reports whether this workspace's install granted the scope needed to receive direct messages.

Slack DOES NOT GRANT NEW SCOPES TO EXISTING INSTALLS: a workspace that connected before im:history was requested keeps its old grant until someone re-runs the install. That is a user-visible migration, not a deploy, so the state is surfaced (in the integration UI, in the docs, and as a gauge) rather than left to look like an empty inbox.

func (*SlackSettings) IngestsAllThreadReplies

func (s *SlackSettings) IngestsAllThreadReplies() bool

IngestsAllThreadReplies reports whether this Slack integration captures every human thread reply as an incident comment. Anything other than an explicit "all" — including an absent value on a pre-existing row — means no.

func (*SlackSettings) ToJSONMap

func (s *SlackSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts SlackSettings to JSONMap for storage.

type StateEntry

type StateEntry struct {
	UID             string     `bun:"uid,pk"`
	OrganizationUID *string    `bun:"organization_uid"`
	UserUID         *string    `bun:"user_uid"`
	Key             string     `bun:"key,notnull"`
	Value           *JSONMap   `bun:"value,type:jsonb"`
	ExpiresAt       *time.Time `bun:"expires_at"`
	CreatedAt       time.Time  `bun:"created_at,notnull"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

StateEntry represents a key-value state entry. Used for storing notification state (Slack threads, Discord messages), user-scoped tokens (email confirmation, password reset), and distributed locking (preventing duplicate notifications).

func NewStateEntry

func NewStateEntry(orgUID *string, key string) *StateEntry

NewStateEntry creates a new state entry with generated UID.

func NewUserStateEntry

func NewUserStateEntry(userUID, key string) *StateEntry

NewUserStateEntry creates a new user-scoped state entry.

type StatusPage

type StatusPage struct {
	UID             string  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string  `bun:"organization_uid,notnull"`
	Name            string  `bun:"name,notnull"`
	Slug            string  `bun:"slug,notnull"`
	Description     *string `bun:"description"`
	Visibility      string  `bun:"visibility,notnull"`
	IsDefault       bool    `bun:"is_default,notnull"`
	// Enabled, ShowAvailability and ShowResponseTime all default to TRUE in the
	// DDL and all three are legal as `false` on create, so none of them may
	// carry a `default:` clause here — see the AutoPublishDelaySeconds note
	// below for why the tag would make the zero value unwritable. NewStatusPage
	// supplies the true-by-default, and the DDL default still covers rows
	// inserted outside the application (spec 2026-08-30-04).
	Enabled          bool `bun:"enabled,notnull"`
	ShowAvailability bool `bun:"show_availability,notnull"`
	ShowResponseTime bool `bun:"show_response_time,notnull"`
	// HistoryDays is the deprecated back-compat column; HistoryPeriod is the
	// source of truth and NewStatusPage sets both. Its `default:90` was dropped
	// with the rest rather than kept as the one surviving exception: nothing
	// writes 0 here, so the tag bought nothing, and keeping it would mean
	// carrying a permanent allowlist entry in the guard test below for a
	// deprecated column (spec 2026-08-30-04, "open question" — decided: drop).
	HistoryDays   int     `bun:"history_days,notnull"`
	HistoryPeriod string  `bun:"history_period,notnull"`
	Language      *string `bun:"language"`
	// AutoPublish turns the incident auto-publication pipeline on for this
	// page. The DDL default is FALSE so that upgrading an existing
	// installation never makes yesterday's internal blips public; NEW pages
	// opt in through NewStatusPage instead (spec 2026-08-19-08).
	AutoPublish bool `bun:"auto_publish,notnull"`
	// AutoPublishDelaySeconds debounces publication: an incident must still be
	// open this long after it opened before customers hear about it.
	//
	// 0 is legal and means "publish immediately", which is why the bun tag
	// carries NO `default:` clause even though the column has one. bun omits a
	// zero-valued field from an INSERT when the tag declares a default, so
	// `default:60` here would silently turn an operator's deliberate "publish
	// immediately" into a one-minute delay — the value would never reach the
	// database at all. The DDL default still applies to rows inserted outside
	// the application (an upgraded installation's existing pages).
	AutoPublishDelaySeconds int `bun:"auto_publish_delay_seconds,notnull"`
	// AutoResolve decides what an auto-created publication does when its
	// incident resolves: always | if_untouched | never.
	AutoResolve string `bun:"auto_resolve,notnull"`
	// CustomCSS is operator-authored CSS injected into the public status page
	// as a <style> text node (never dangerouslySetInnerHTML). nil = none.
	// Capped at 64 KB and @import-free by API validation; unlike the
	// custom-domain columns it IS exposed on public responses, since the
	// public renderer is its only consumer.
	CustomCSS *string `bun:"custom_css"`
	// CustomDomain is a customer-owned hostname (punycode/ASCII, lowercased)
	// the page is served on. nil = none. Globally unique among live rows.
	CustomDomain *string `bun:"custom_domain"`
	// CustomDomainToken is the opaque, DNS-label-safe token (lowercase base32),
	// set while a domain is configured; in token mode it is the leading label of
	// the expected CNAME target. Never exposed on public endpoints.
	CustomDomainToken *string `bun:"custom_domain_token"`
	// CustomDomainVerifiedAt is when the domain last passed CNAME verification.
	// nil = unverified — only verified pages are served on the custom host.
	CustomDomainVerifiedAt *time.Time `bun:"custom_domain_verified_at"`
	// CustomDomainCheckedAt is when the periodic re-verification job last
	// checked this domain.
	CustomDomainCheckedAt *time.Time `bun:"custom_domain_checked_at"`
	// CustomDomainFailures counts consecutive re-verification failures. At
	// CustomDomainGraceAfterFailures the domain enters `grace` (still served);
	// only at CustomDomainHardDemoteAfterFailures is the verification cleared
	// (domain release/takeover protection).
	CustomDomainFailures int `bun:"custom_domain_failures,notnull"`
	// CustomDomainState is the explicit lifecycle state — one of the
	// CustomDomainState* constants. It exists because a single failure counter
	// was doing two jobs ("flaky right now" and "gone for good"), which made a
	// DNS blip indistinguishable from a domain transfer and took status pages
	// dark permanently (spec 2026-08-23-03). NewStatusPage sets it — the tag
	// must not carry the DDL's `default:'none'`, or bun would drop any state
	// the Go side sets to its zero value from the INSERT entirely.
	CustomDomainState string `bun:"custom_domain_state,notnull"`
	// CustomDomainSuccesses counts consecutive SUCCESSFUL re-verifications. It
	// is the counter re-promotion is earned with: a demoted domain needs
	// CustomDomainRepromoteSuccesses of them in a row (plus a still-valid
	// certificate) before the sweep trusts it again.
	CustomDomainSuccesses int `bun:"custom_domain_successes,notnull"`
	// CustomDomainGraceSince is when the domain last entered `grace`. nil
	// outside grace. Makes "how long has this been degrading" readable instead
	// of inferred from a counter times a job interval.
	CustomDomainGraceSince *time.Time `bun:"custom_domain_grace_since"`
	// CustomDomainLastCheck is a human-readable diagnostic from the last
	// re-verification: the mode used, the expected CNAME target, what DNS
	// actually returned, and the lookup error if any. Without it, "verification
	// fails but dig says the record is right" is only investigable by
	// correlating server logs with manual dig runs.
	CustomDomainLastCheck *string `bun:"custom_domain_last_check"`
	// PasswordHash is the password hash gating a `visibility = password` page.
	// It is produced by internal/utils/passwords, whose active policy is
	// argon2id by default (bcrypt is selectable) — the same policy user
	// passwords use, so there is one hashing decision in the system, not two.
	// It is NEVER serialized onto any response — reads expose `hasPassword`
	// only. nil on public/private pages.
	PasswordHash *string `bun:"password_hash"`
	// KioskTokenHash is the sha256 hex of the page's kiosk token (spec
	// 2026-08-29-08) — the long-lived, revocable, read-only grant a wallboard
	// screen presents as `?kiosk=<token>` so it can render a `password` or
	// `private` page unattended.
	//
	// sha256 rather than the argon2id used for PasswordHash, and that is a
	// deliberate difference rather than an inconsistency: the token is 32
	// bytes of CSPRNG output with no dictionary to slow down, and a TV
	// re-presents it every 15-30 s, so a memory-hard verification per request
	// would buy nothing and cost a lot.
	//
	// Like PasswordHash it is NEVER serialized — reads expose `hasKioskToken`
	// only, and the plaintext token is shown exactly once, at mint time. nil
	// means the page has no kiosk token.
	KioskTokenHash *string `bun:"kiosk_token_hash"`
	// Settings holds per-page display customization — availability color
	// thresholds and the page's brand identity (logo, favicon, white-label
	// opt-in) — typed rather than a free-form map so keys stay discoverable
	// (specs 2026-08-03-01, 2026-08-22-03). Column is NOT NULL DEFAULT '{}'.
	//
	// Read the branding keys through the Settings accessors
	// (Settings.LogoFileUID(), .FaviconFileUID(), .HideBranding()) rather than
	// reaching into .Branding, which is nil on a page that never set one.
	Settings  StatusPageSettings `bun:"settings,type:jsonb,notnull"`
	CreatedAt time.Time          `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time          `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt *time.Time         `bun:"deleted_at"`
}

StatusPage represents a public status page for an organization.

func NewStatusPage

func NewStatusPage(orgUID, name, slug string) *StatusPage

NewStatusPage creates a new status page with generated UID.

type StatusPageBrandingUpdate

type StatusPageBrandingUpdate struct {
	LogoFileUID    *string
	FaviconFileUID *string
	HideBranding   bool
}

StatusPageBrandingUpdate is the whole-SECTION writer for a status page's branding: it replaces `settings -> branding` in full on every call, so one shape expresses set, replace and clear and no caller can accidentally leave a stale file UID behind and keep a retired blob publicly reachable.

"In full" stops at the branding section. The write is a two-level JSON merge in SQL (see UpdateStatusPageBranding in each dialect), never a read-modify-write of the whole settings column in Go — that would clobber a concurrent `availability` change, which is the single most likely regression this storage move could introduce.

func (*StatusPageBrandingUpdate) BrandingPatchJSON

func (u *StatusPageBrandingUpdate) BrandingPatchJSON() (string, error)

BrandingPatchJSON returns the merge patch for the `branding` SECTION alone — what Postgres concatenates onto `settings->'branding'`.

func (*StatusPageBrandingUpdate) SettingsPatchJSON

func (u *StatusPageBrandingUpdate) SettingsPatchJSON() (string, error)

SettingsPatchJSON returns the merge patch rooted at `settings` — what SQLite hands to json_patch.

type StatusPageCustomDomainUpdate

type StatusPageCustomDomainUpdate struct {
	Domain     *string
	Token      *string
	VerifiedAt *time.Time
	CheckedAt  *time.Time
	Failures   int
	// State is the lifecycle state to write. The zero value is deliberately
	// NOT a legal state: every caller states its intent, and the DB layer
	// normalizes "" to CustomDomainStateNone so a forgotten field clears the
	// domain rather than silently keeping a stale `active`.
	State string
	// Successes is the consecutive-success counter (see the model field).
	Successes int
	// GraceSince is when the domain entered grace; nil clears it.
	GraceSince *time.Time
	// LastCheck is the diagnostic string from the last re-verification; nil
	// clears it.
	LastCheck *string
}

StatusPageCustomDomainUpdate is the whole-lifecycle writer for a status page's custom-domain columns. Every field is written verbatim (a full overwrite of all five columns plus updated_at), so it expresses every transition — set, clear, verify-now, and the periodic re-verify job — in one shape. Domain/Token nil clears the columns to NULL; VerifiedAt/CheckedAt nil clears those timestamps.

type StatusPagePeriod

type StatusPagePeriod string

StatusPagePeriod is the history window a status page renders. It mirrors the badge uptime-bar vocabulary: 24h is hourly (24 one-hour buckets), the rest are daily (N 24-hour buckets). It is the source of truth for bucketing; HistoryDays is kept populated for one release for backward-compat.

const (
	StatusPagePeriod24h StatusPagePeriod = "24h"
	StatusPagePeriod7d  StatusPagePeriod = "7d"
	StatusPagePeriod30d StatusPagePeriod = "30d"
	StatusPagePeriod90d StatusPagePeriod = "90d"
)

StatusPagePeriod values.

func PeriodFromDays

func PeriodFromDays(days int) StatusPagePeriod

PeriodFromDays maps a legacy history_days count to the closest period enum, used to backfill rows and to accept the deprecated historyDays input for one release. 7→7d, 30→30d, everything else (including 90) →90d.

func (StatusPagePeriod) IsHourly

func (p StatusPagePeriod) IsHourly() bool

IsHourly reports whether the period buckets by hour (only 24h does).

func (StatusPagePeriod) Valid

func (p StatusPagePeriod) Valid() bool

Valid reports whether the period is one of the four supported values.

type StatusPageResource

type StatusPageResource struct {
	UID        string `bun:"uid,pk,type:varchar(36)"`
	SectionUID string `bun:"section_uid,notnull"`
	// CheckUID is the individual check to display. nil when the resource
	// targets a group.
	CheckUID *string `bun:"check_uid"`
	// CheckGroupUID is the check group to display as one aggregated component.
	// nil when the resource targets an individual check.
	CheckGroupUID *string `bun:"check_group_uid"`
	PublicName    *string `bun:"public_name"`
	Explanation   *string `bun:"explanation"`
	// AutoPublish overrides the page-level auto-publish setting for this one
	// resource. nil (the default) means "inherit the page" — it is a
	// three-state column on purpose, so a page can be flipped on or off
	// without silently rewriting every resource's intent.
	AutoPublish *bool `bun:"auto_publish"`
	// ManagedBySelector marks a row the section's selector owns: the
	// reconciler created it and the reconciler will remove it when the check
	// stops matching (spec 2026-08-29-11). Operators cannot delete or reorder
	// a managed row — the selector is the source of truth for it.
	//
	// The inverse — a MANUAL row — is never touched by the reconciler, which
	// is what makes "manual placement wins" true rather than a race.
	ManagedBySelector bool      `bun:"managed_by_selector,notnull"`
	Position          int       `bun:"position,notnull"`
	CreatedAt         time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt         time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

StatusPageResource represents a check OR a check group assigned to a status page section (spec 2026-08-01-03). Exactly one of CheckUID / CheckGroupUID is set — the database enforces it with an XOR check constraint, mirroring MaintenanceWindowCheck.

A group resource renders as ONE public component: rolled-up status, weighted average availability across members, and maintenance from a group- or member-targeted window. Members are never listed publicly.

func NewManagedStatusPageResource

func NewManagedStatusPageResource(sectionUID, checkUID string, position int) *StatusPageResource

NewManagedStatusPageResource creates a check-targeting resource OWNED by the section's selector (spec 2026-08-29-11). Identical to NewStatusPageResource apart from the ownership flag, so a materialized row is indistinguishable from a manual one to every reader — which is the whole point of materializing instead of virtualizing.

func NewStatusPageGroupResource

func NewStatusPageGroupResource(sectionUID, checkGroupUID string, position int) *StatusPageResource

NewStatusPageGroupResource creates a new group-targeting resource with generated UID.

func NewStatusPageResource

func NewStatusPageResource(sectionUID, checkUID string, position int) *StatusPageResource

NewStatusPageResource creates a new check-targeting resource with generated UID.

func (*StatusPageResource) IsGroup

func (r *StatusPageResource) IsGroup() bool

IsGroup reports whether the resource targets a check group rather than an individual check.

type StatusPageResourceUpdate

type StatusPageResourceUpdate struct {
	PublicName  *string
	Explanation *string
	Position    *int
	// SetAutoPublish must be true for AutoPublish to be written at all; that
	// is what lets a caller reset the override back to "inherit" (SetAutoPublish
	// true, AutoPublish nil) as distinct from "leave alone".
	SetAutoPublish bool
	AutoPublish    *bool
	// SetTarget switches the resource's target kind. When true, BOTH target
	// columns are written: exactly one of CheckUID / CheckGroupUID must be
	// non-nil and the other column is set to NULL, so the XOR constraint always
	// holds. When false the target is left untouched.
	SetTarget     bool
	CheckUID      *string
	CheckGroupUID *string
}

StatusPageResourceUpdate represents fields that can be updated on a resource.

type StatusPageSection

type StatusPageSection struct {
	UID           string `bun:"uid,pk,type:varchar(36)"`
	StatusPageUID string `bun:"status_page_uid,notnull"`
	Name          string `bun:"name,notnull"`
	Slug          string `bun:"slug,notnull"`
	Position      int    `bun:"position,notnull"`
	// Selector is the section's dynamic-membership rule, or nil for a
	// hand-curated section (the default, and what every pre-existing section
	// stays). Never defaulted to anything non-nil: auto-inclusion has to be an
	// explicit, deliberate act (spec 2026-08-29-11).
	Selector  *SectionSelector `bun:"selector,type:jsonb"`
	CreatedAt time.Time        `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time        `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt *time.Time       `bun:"deleted_at"`
}

StatusPageSection represents a section within a status page.

func NewStatusPageSection

func NewStatusPageSection(pageUID, name, slug string, position int) *StatusPageSection

NewStatusPageSection creates a new section with generated UID.

type StatusPageSectionUpdate

type StatusPageSectionUpdate struct {
	Name     *string
	Slug     *string
	Position *int
	// SetSelector must be true for Selector to be written at all; that is what
	// lets a caller CLEAR a selector (SetSelector true, Selector nil — the
	// section reverts to hand-curated) as distinct from "leave alone".
	SetSelector bool
	Selector    *SectionSelector
}

StatusPageSectionUpdate represents fields that can be updated on a section.

type StatusPageSettings

type StatusPageSettings struct {
	Availability *AvailabilitySettings `json:"availability,omitempty"`
	// Branding is the page's brand identity — logo, favicon, white-label
	// opt-in (spec 2026-08-22-03). It lives here rather than in three columns
	// of its own because it is read only while RENDERING the page: nothing
	// filters, joins or uniquely constrains on it, which is the rule written
	// down in wiki/conventions/database.md.
	Branding *BrandingSettings `json:"branding,omitempty"`
}

StatusPageSettings is the typed decode target for status_pages.settings (Postgres jsonb / SQLite text). It is the home for per-page display customization knobs — availability thresholds and brand identity — added without a two-dialect migration each time. Keep it a typed struct, not a free-form map, so keys stay discoverable and validation lives in one place.

func (StatusPageSettings) EffectiveThresholds

func (s StatusPageSettings) EffectiveThresholds() (float64, float64)

EffectiveThresholds resolves the page's effective availability thresholds, nil-safe on the Availability section (StatusPageSettings itself is always a value, never nil, since it is stored NOT NULL DEFAULT '{}').

func (StatusPageSettings) FaviconFileUID

func (s StatusPageSettings) FaviconFileUID() *string

FaviconFileUID returns the page's favicon file UID, nil when unset.

func (StatusPageSettings) HideBranding

func (s StatusPageSettings) HideBranding() bool

HideBranding reports the page's stored white-label opt-in (false when the section is absent). It is only half of the decision — see BrandingSettings.

func (StatusPageSettings) LogoFileUID

func (s StatusPageSettings) LogoFileUID() *string

LogoFileUID returns the page's logo file UID, nil when unset. Nil-safe on the section so no caller has to check it.

func (*StatusPageSettings) Scan

func (s *StatusPageSettings) Scan(value any) error

Scan implements sql.Scanner for reading the JSON blob back from either engine.

func (StatusPageSettings) Value

func (s StatusPageSettings) Value() (driver.Value, error)

Value implements driver.Valuer so StatusPageSettings persists as a JSON string on both Postgres (jsonb) and SQLite (text). The zero value marshals to "{}", matching the column's NOT NULL DEFAULT '{}'.

type StatusPageSubscriber

type StatusPageSubscriber struct {
	bun.BaseModel `bun:"table:status_page_subscriber"`

	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull"`
	StatusPageUID   string `bun:"status_page_uid,notnull"`
	// Email is nil for a webhook/slack subscriber.
	Email            *string           `bun:"email"`
	Channel          SubscriberChannel `bun:"channel,notnull"`
	ConfirmedAt      *time.Time        `bun:"confirmed_at"`
	ConfirmToken     string            `bun:"confirm_token,notnull"`
	UnsubscribeToken string            `bun:"unsubscribe_token,notnull"`
	Scope            SubscriberScope   `bun:"scope,notnull"`
	IncidentUID      *string           `bun:"incident_uid"`
	// EndpointPrivate is the AES-GCM envelope holding {url, signingSecret} for
	// an endpoint channel. NEVER serialized.
	EndpointPrivate *string `bun:"endpoint_private"`
	// EndpointHint is the masked remnant of the URL — the only part a response
	// may echo.
	EndpointHint *string `bun:"endpoint_hint"`
	// EndpointKey is the sha256 of the normalized URL, used only as the
	// dedup key in the live-uniqueness index so the URL itself never lands in
	// an index.
	EndpointKey *string `bun:"endpoint_key"`
	// FailureCount counts CONSECUTIVE delivery failures; a success resets it.
	FailureCount int `bun:"failure_count,notnull"`
	// DisabledAt is set when the circuit breaker tripped. A disabled row is
	// skipped by the fan-out but kept, so the operator can see it and re-enable
	// it after fixing the endpoint.
	DisabledAt *time.Time `bun:"disabled_at"`
	CreatedAt  time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	DeletedAt  *time.Time `bun:"deleted_at"`
}

StatusPageSubscriber is a subscription to a status page's updates. The original (and only public self-serve) kind is an email address, double opt-in: a row is inactive (ConfirmedAt nil) until the visitor clicks the confirm link. Operators can additionally register webhook and Slack deliveries, which are created already confirmed — the operator IS the verification.

Email is PII: never log it in clear, never expose it in public API responses. The endpoint URL is treated with the SAME opacity and then some: it is a credential, so it lives encrypted in EndpointPrivate and only EndpointHint ever reaches a response.

func NewEndpointSubscriber

func NewEndpointSubscriber(
	orgUID, statusPageUID string, channel SubscriberChannel, scope SubscriberScope,
) *StatusPageSubscriber

NewEndpointSubscriber creates an operator-registered webhook/slack subscription. Unlike an email subscription it is born CONFIRMED: double opt-in exists to stop someone signing up a stranger's mailbox, and an operator registering their own webhook on their own page is not that.

func NewStatusPageSubscriber

func NewStatusPageSubscriber(orgUID, statusPageUID, email string, scope SubscriberScope) *StatusPageSubscriber

NewStatusPageSubscriber creates an unconfirmed subscriber with a generated UID and CreatedAt. Tokens are set by the service.

func (*StatusPageSubscriber) EmailAddress

func (s *StatusPageSubscriber) EmailAddress() string

EmailAddress returns the subscriber's email, or "" for an endpoint channel.

type StatusPageUpdate

type StatusPageUpdate struct {
	Name             *string
	Slug             *string
	Description      *string
	Visibility       *string
	IsDefault        *bool
	Enabled          *bool
	ShowAvailability *bool
	ShowResponseTime *bool
	HistoryDays      *int
	HistoryPeriod    *string
	Language         *string
	// AutoPublish / AutoPublishDelaySeconds / AutoResolve are the incident
	// auto-publication settings (spec 2026-08-19-08). nil leaves untouched.
	AutoPublish             *bool
	AutoPublishDelaySeconds *int
	AutoResolve             *string
	// CustomCSS updates the page's custom stylesheet. A pointer to the empty
	// string clears the column (the appearance editor's "empty textarea"), a
	// nil pointer leaves it untouched.
	CustomCSS *string
	// HideBranding flips the page-level white-label opt-in, stored in
	// `settings -> branding -> hideBranding`. nil leaves it. The DB layer
	// merges it into the JSON rather than overwriting the column — and folds
	// it into Settings when that is set in the same call, since two
	// `SET settings = ...` clauses in one UPDATE is a Postgres error.
	HideBranding *bool
	// PasswordHash writes the password hash gating a password page. A pointer to
	// the empty string CLEARS the column (the page stopped being password
	// protected); a nil pointer leaves it untouched, which is what keeps an
	// unrelated PATCH from silently unlocking a page.
	PasswordHash *string
	// KioskTokenHash writes the page's kiosk token hash. Same three-state
	// convention as PasswordHash: nil leaves it untouched, a pointer to the
	// empty string CLEARS it (revoke), and a value replaces it — which is what
	// makes "regenerate" invalidate the previous token with no extra bookkeeping.
	KioskTokenHash *string
	// Settings overwrites the whole settings column when non-nil (the caller
	// — statuspages.Service — has already applied the no-deep-merge
	// section-replace-or-reset semantics against the current value). A nil
	// pointer leaves the column untouched.
	Settings *StatusPageSettings
}

StatusPageUpdate represents fields that can be updated on a status page.

type StatusUpdate

type StatusUpdate struct {
	UID             string  `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string  `bun:"organization_uid,notnull"`
	StatusPageUID   string  `bun:"status_page_uid,notnull"`
	SectionUID      *string `bun:"section_uid"`  // optional: scope to section
	CheckUID        *string `bun:"check_uid"`    // optional: scope to check
	IncidentUID     *string `bun:"incident_uid"` // optional: thread under incident
	// IncidentPublicationUID threads the update under a publication. A
	// hand-authored publication has no incident to thread on, so this is the
	// only link it gets (spec 2026-08-19-08).
	IncidentPublicationUID *string          `bun:"incident_publication_uid"`
	Title                  string           `bun:"title,notnull"`
	BodyMarkdown           string           `bun:"body_markdown,notnull"`
	LinkURL                *string          `bun:"link_url"`
	Kind                   StatusUpdateKind `bun:"kind,notnull"`
	PublishedAt            time.Time        `bun:"published_at,notnull,default:current_timestamp"`
	// AuthorUID is the user who posted the update. nil = generated by the
	// auto-publish pipeline: a machine post has no author, and attributing it
	// to whichever human happens to own the org would be a lie the UI renders.
	AuthorUID *string    `bun:"author_uid"`
	CreatedAt time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt *time.Time `bun:"deleted_at"`
}

StatusUpdate is an operator-written narrative post anchored to a status page.

func NewStatusUpdate

func NewStatusUpdate(orgUID, statusPageUID, authorUID string) *StatusUpdate

NewStatusUpdate creates a new StatusUpdate with generated UID and timestamps.

type StatusUpdateKind

type StatusUpdateKind string

StatusUpdateKind represents the kind of a status update.

const (
	// StatusUpdateKindInvestigating indicates the team is investigating.
	StatusUpdateKindInvestigating StatusUpdateKind = "investigating"
	// StatusUpdateKindIdentified indicates the issue has been identified.
	StatusUpdateKindIdentified StatusUpdateKind = "identified"
	// StatusUpdateKindMonitoring indicates the fix is being monitored.
	StatusUpdateKindMonitoring StatusUpdateKind = "monitoring"
	// StatusUpdateKindResolved indicates the issue is resolved.
	StatusUpdateKindResolved StatusUpdateKind = "resolved"
	// StatusUpdateKindMaintenance indicates a scheduled maintenance.
	StatusUpdateKindMaintenance StatusUpdateKind = "maintenance"
	// StatusUpdateKindInfo is a general informational update.
	StatusUpdateKindInfo StatusUpdateKind = "info"
)

func (StatusUpdateKind) IsValid

func (k StatusUpdateKind) IsValid() bool

IsValid returns true if the kind is one of the recognized values.

type StatusUpdatesFilter

type StatusUpdatesFilter struct {
	StatusPageUID string
	SectionUID    *string
	CheckUID      *string
	IncidentUID   *string
	// IncidentPublicationUID filters to one publication's thread.
	IncidentPublicationUID *string
	Limit                  int
	Offset                 int
}

StatusUpdatesFilter configures what to return from ListStatusUpdates.

type SubscriberChannel

type SubscriberChannel string

SubscriberChannel is where a subscription delivers.

`email` is the public self-serve channel and the only one a visitor can create; `webhook` and `slack` are operator-created from the dashboard, because a random visitor pasting an incoming-webhook URL has no verification story — see the spec's note on operator-side management.

const (
	SubscriberChannelEmail   SubscriberChannel = "email"
	SubscriberChannelWebhook SubscriberChannel = "webhook"
	SubscriberChannelSlack   SubscriberChannel = "slack"
)

Subscriber channels.

func (SubscriberChannel) IsEndpoint

func (c SubscriberChannel) IsEndpoint() bool

IsEndpoint reports whether the channel delivers to a URL rather than a mailbox — i.e. whether the row carries an encrypted endpoint.

func (SubscriberChannel) IsValid

func (c SubscriberChannel) IsValid() bool

IsValid reports whether the channel is one of the recognized values.

type SubscriberScope

type SubscriberScope string

SubscriberScope determines how broadly a subscriber is notified.

const (
	// SubscriberScopePage notifies on every published update for the page.
	SubscriberScopePage SubscriberScope = "page"
	// SubscriberScopeIncident notifies only for updates threaded under a
	// specific incident.
	SubscriberScopeIncident SubscriberScope = "incident"
)

func (SubscriberScope) IsValid

func (s SubscriberScope) IsValid() bool

IsValid reports whether the scope is one of the recognized values.

type SupportMessage

type SupportMessage struct {
	bun.BaseModel `bun:"table:support_messages,alias:support_messages"`

	UID        string    `bun:"uid,pk,type:varchar(36)"`
	ThreadUID  string    `bun:"thread_uid,notnull,type:varchar(36)"`
	Channel    string    `bun:"channel,notnull"`
	Direction  string    `bun:"direction,notnull"`
	Body       string    `bun:"body,notnull"`
	Truncated  bool      `bun:"truncated,notnull"`
	RawType    string    `bun:"raw_type,notnull"`
	ExternalID *string   `bun:"external_id"`
	AuthorUID  *string   `bun:"author_uid"`
	Delivery   JSONMap   `bun:"delivery,type:jsonb,nullzero"`
	CreatedAt  time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt  time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

SupportMessage is one message inside a thread.

func NewSupportMessage

func NewSupportMessage(threadUID, channel, direction, body string, createdAt time.Time) *SupportMessage

NewSupportMessage builds a message row, applying the body cap.

type SupportReplyWindow

type SupportReplyWindow struct {
	// Expires reports whether this channel has a window at all. Only WhatsApp
	// does.
	Expires bool `json:"expires"`
	// Open reports whether a free-form reply can be sent right now.
	Open bool `json:"open"`
	// ExpiresAt is when the window closes. Nil for non-expiring channels.
	ExpiresAt *time.Time `json:"expiresAt,omitempty"`
	// Reason explains a closed window in operator-facing terms. Empty when open.
	Reason string `json:"reason,omitempty"`
	// CostsMoney flags channels where every reply is billed per segment.
	CostsMoney bool `json:"costsMoney"`
}

SupportReplyWindow is the DERIVED answer to "can we still send a free-form reply right now?". It is computed from the last inbound message's timestamp and the channel's rule at read time and never stored, so it cannot go stale.

It is a different axis from Status. A thread can be open (the customer's question is unanswered) and yet expired (WhatsApp will no longer accept a free-form reply) — which is precisely the state an operator most needs to see, because it is the one where the product cannot do what the UI otherwise implies.

type SupportThread

type SupportThread struct {
	bun.BaseModel `bun:"table:support_threads,alias:support_threads"`

	UID             string     `bun:"uid,pk,type:varchar(36)"`
	Channel         string     `bun:"channel,notnull"`
	ChannelIdentity string     `bun:"channel_identity,notnull"`
	ChannelContext  JSONMap    `bun:"channel_context,type:jsonb,nullzero"`
	Subject         string     `bun:"subject,notnull"`
	Status          string     `bun:"status,notnull"`
	OrganizationUID *string    `bun:"organization_uid"`
	UserUID         *string    `bun:"user_uid"`
	LastMessageAt   time.Time  `bun:"last_message_at,notnull"`
	LastInboundAt   *time.Time `bun:"last_inbound_at"`
	UnreadCount     int        `bun:"unread_count,notnull"`
	LastMirrorAt    *time.Time `bun:"last_mirror_at"`
	PendingMirrors  int        `bun:"pending_mirrors,notnull"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`
}

SupportThread is one conversation with one person on one channel.

Threads belong to the INSTANCE, not to an organization. The sender of an inbound WhatsApp message is a phone number; frequently there is no org to attribute it to at all, and a message from a stranger must not be dropped for lack of one. OrganizationUID/UserUID are therefore nullable ATTRIBUTION — a hint for the operator, never an access-control boundary.

func NewSupportThread

func NewSupportThread(channel, identity string, createdAt time.Time) *SupportThread

NewSupportThread builds a live thread for a channel identity.

func (*SupportThread) ReplyWindow

func (t *SupportThread) ReplyWindow(now time.Time) SupportReplyWindow

ReplyWindow derives the current reply window for the thread.

type TLSStorageEntry

type TLSStorageEntry struct {
	bun.BaseModel `bun:"table:tls_storage"`

	Key        string    `bun:"key,pk"`
	Value      []byte    `bun:"value,notnull"`
	ModifiedAt time.Time `bun:"modified_at,notnull"`
}

TLSStorageEntry is one asset in the certmagic key-value store backing in-server ACME (spec 2026-07-26-01). Keys are certmagic's own path-like namespace (slash-separated, no leading/trailing slash), values are opaque bytes.

SECURITY: values include ACME account keys and certificate PRIVATE KEYS. Nothing outside internal/tlsedge may read this table, and it must never be exposed through an API, export, or debug surface.

type TLSStorageKeyInfo

type TLSStorageKeyInfo struct {
	Key        string
	Size       int64
	ModifiedAt time.Time
}

TLSStorageKeyInfo is the metadata certmagic's Stat/List need about a stored key. It deliberately carries no value bytes so listings never pull private keys into memory.

type TLSStorageLock

type TLSStorageLock struct {
	bun.BaseModel `bun:"table:tls_storage_locks"`

	Key       string    `bun:"key,pk"`
	Owner     string    `bun:"owner,notnull"`
	ExpiresAt time.Time `bun:"expires_at,notnull"`
}

TLSStorageLock is an expiring, cluster-wide lock row backing certmagic's Locker interface. The holder refreshes ExpiresAt while it works; a stale row may be taken over by any node, which is what keeps a crashed issuance from wedging renewals forever.

type TokenType

type TokenType string

TokenType represents the type of user token.

const (
	// TokenTypePAT represents a Personal Access Token.
	TokenTypePAT TokenType = "pat"
	// TokenTypeRefresh represents a refresh token for session management.
	TokenTypeRefresh TokenType = "refresh"
	// TokenTypeOAuthRefresh represents a rotating OAuth 2.1 refresh grant for
	// the MCP resource (spec 2026-06-20-03). The grant's client_id, scope, and
	// resource bindings ride in Properties; revocation is the row's soft
	// delete. Each redemption endpoint validates its own type, so the three
	// types can never be exchanged for one another.
	TokenTypeOAuthRefresh TokenType = "oauth_refresh"
)

type TwilioSettings

type TwilioSettings struct {
	AccountSID          string   `json:"account_sid"`
	AuthToken           string   `json:"auth_token,omitempty"`
	FromNumber          string   `json:"from_number,omitempty"`
	MessagingServiceSID string   `json:"messaging_service_sid,omitempty"`
	VoiceFromNumber     string   `json:"voice_from_number,omitempty"`
	ToNumbers           []string `json:"to_numbers,omitempty"`
	// Region is the Twilio regional edition this account was provisioned in
	// ("" or "us1" = the default global/US1 edge, "ie1" = Ireland, "au1" =
	// Australia, ...). Empty behaves exactly as before this field existed.
	Region string `json:"region,omitempty"`
}

TwilioSettings is the public (queryable) side of a Twilio connection's Settings JSONB. The matching secret — the account auth token — lives encrypted in SettingsPrivate under the "auth_token" key (see connectionSecretFields). Exactly one of FromNumber / MessagingServiceSID is set for SMS; VoiceFromNumber (optional) enables voice calls; ToNumbers are shared recipients for direct-channel (registry-path) sends. Region is public/non-secret: it picks which Twilio API host requests go to (see twilio.BaseURLForRegion) but carries no credential material itself.

func TwilioSettingsFromJSONMap

func TwilioSettingsFromJSONMap(m JSONMap) (*TwilioSettings, error)

TwilioSettingsFromJSONMap parses TwilioSettings from a JSONMap. The map is expected to be the decrypt-and-merged Settings (auth_token present).

func (*TwilioSettings) ToJSONMap

func (ts *TwilioSettings) ToJSONMap() (JSONMap, error)

ToJSONMap converts TwilioSettings to JSONMap for storage.

type User

type User struct {
	UID               string     `bun:"uid,pk,type:varchar(36)"`
	Email             string     `bun:"email,notnull"`
	Name              string     `bun:"name"`
	AvatarURL         string     `bun:"avatar_url"`
	PasswordHash      *string    `bun:"password_hash"`
	EmailVerifiedAt   *time.Time `bun:"email_verified_at"`
	SuperAdmin        bool       `bun:"super_admin"`
	TOTPSecret        *string    `bun:"totp_secret"`
	TOTPEnabled       bool       `bun:"totp_enabled,notnull"`
	TOTPRecoveryCodes []string   `bun:"totp_recovery_codes,type:jsonb"`
	// MustChangePassword forces a password rotation before the account can do
	// anything else. It is a GENERAL user-level capability, not a property of
	// the seeded bootstrap admin: an operator-initiated reset, an invited user
	// or a compromised-credential response all set the same flag, and every
	// consumer reads this field rather than keying on who the user is.
	//
	// While it is true, a session authenticated as this user reaches only the
	// rotation endpoint, /auth/me and /auth/logout — enforced centrally in the
	// auth layer (see internal/handlers/auth/password_rotation.go), so the API,
	// the dashboard, the CLI, PAT creation and the realtime socket are all
	// covered by one rule.
	//
	// Defaults to false, which is what keeps OAuth/SSO/LDAP users — who may
	// carry a nil PasswordHash and could not satisfy a rotation — unaffected.
	MustChangePassword bool `bun:"must_change_password,notnull"`
	// Demo marks the shared public-demo principal (spec 2026-09-06-02). Like
	// MustChangePassword it is a GENERAL user-level capability, not a property
	// of one seeded email: the JWT/PAT claims minted for such a user carry it,
	// the write guard in RequireAuth keys off it, and the demo cleanup job
	// reconciles whatever it names. Nothing anywhere matches on the address.
	//
	// A demo session may write exactly four things (see
	// handlers/auth/demo_guard.go); everything else answers 403 DEMO_READ_ONLY.
	Demo bool `bun:"demo,notnull"`
	// SignupAttribution records where the signup came from — the campaign tags
	// and ad click identifier the marketing site forwarded on the link that
	// led here (spec 2026-09-07-03). Set once, at account creation, and never
	// updated: it is a historical fact about the signup, not a live property
	// of the user. Nil for every account that did not arrive from a tagged
	// link, which is most of them. Stored as JSON so the shape can grow
	// (another ad network's click id) without a migration.
	SignupAttribution *SignupAttribution `bun:"signup_attribution,type:jsonb"`
	LastActiveAt      *time.Time         `bun:"last_active_at"`
	CreatedAt         time.Time          `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt         time.Time          `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt         *time.Time         `bun:"deleted_at"`
}

User represents a global user account.

func NewUser

func NewUser(email string) *User

NewUser creates a new user with generated UID.

type UserContact

type UserContact struct {
	bun.BaseModel `bun:"table:user_contacts"`

	UID             string     `bun:"uid,pk,type:varchar(36)"`
	UserUID         string     `bun:"user_uid,notnull,type:varchar(36)"`
	OrganizationUID string     `bun:"organization_uid,notnull,type:varchar(36)"`
	Type            string     `bun:"type,notnull"`
	Value           string     `bun:"value,notnull"`
	Label           string     `bun:"label,notnull"`
	VerifiedAt      *time.Time `bun:"verified_at"`
	// Verification state for contact types that require a code round-trip
	// (phone). VerifyCodeHash is the SHA-256 hex of the in-flight 6-digit
	// code; nil when no verification is pending or after a successful confirm.
	VerifyCodeHash  *string    `bun:"verify_code_hash"`
	VerifyExpiresAt *time.Time `bun:"verify_expires_at"`
	VerifyAttempts  int        `bun:"verify_attempts,notnull"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at,soft_delete"`
}

UserContact is one addressable endpoint for a user (email, phone, Slack DM, …). A single contact can belong to only one org — contacts are org-scoped so the correct bot token can be resolved at dispatch time.

func NewUserContact

func NewUserContact(userUID, orgUID, contactType, value, label string) *UserContact

NewUserContact builds a contact with a fresh UID.

type UserIntegrationIdentity

type UserIntegrationIdentity struct {
	bun.BaseModel `bun:"table:user_integration_identities,alias:uii"`

	UID             string `bun:"uid,pk,type:varchar(36)"`
	OrganizationUID string `bun:"organization_uid,notnull,type:varchar(36)"`
	IntegrationUID  string `bun:"integration_uid,notnull,type:varchar(36)"`
	UserUID         string `bun:"user_uid,notnull,type:varchar(36)"`
	// ExternalID is the provider-side identifier used to address the person in
	// a message (a Slack user id, rendered as `<@U123ABC>`).
	ExternalID string `bun:"external_id,notnull"`
	// DisplayName is the provider-side display name captured at match time.
	// Purely cosmetic — used for the admin UI and for the plain-text fallback.
	DisplayName string `bun:"display_name,notnull"`
	// Source is IdentitySourceAuto or IdentitySourceManual.
	Source    string    `bun:"source,notnull"`
	CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}

UserIntegrationIdentity answers "who is this org member on this integration instance" — e.g. `U123ABC` on a specific Slack workspace. It is deliberately NOT a user_contact: contacts are "how to page me" (and carry verification state), identities are "who I am there" and are used for mentions and attribution only. Paging never reads this table.

Scoped per integration (not per integration *type*) because an org can connect several Slack workspaces, and the same person has a different user id in each.

func NewUserIntegrationIdentity

func NewUserIntegrationIdentity(
	orgUID, integrationUID, userUID, externalID, displayName, source string,
) *UserIntegrationIdentity

NewUserIntegrationIdentity builds an identity row with a fresh UID.

type UserNotificationRoute

type UserNotificationRoute struct {
	bun.BaseModel `bun:"table:user_notification_routes"`

	UID        string `bun:"uid,pk,type:varchar(36)"`
	UserUID    string `bun:"user_uid,notnull,type:varchar(36)"`
	OrgUID     string `bun:"org_uid,notnull,type:varchar(36)"`
	ContactUID string `bun:"contact_uid,notnull,type:varchar(36)"`
	// No `default:true` on the tag even though the column has one — see the
	// StatusPage.AutoPublishDelaySeconds note: it made a route impossible to
	// CREATE disabled. NewUserNotificationRoute supplies the default.
	Enabled   bool      `bun:"enabled,notnull"`
	Position  int       `bun:"position,notnull"`
	CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`

	Contact *UserContact `bun:"rel:belongs-to,join:contact_uid=uid"`
}

UserNotificationRoute joins a UserContact to an ordered, toggle-able delivery slot. One route per contact (enforced by UNIQUE on contact_uid).

func NewUserNotificationRoute

func NewUserNotificationRoute(userUID, orgUID, contactUID string, position int) *UserNotificationRoute

NewUserNotificationRoute builds a route with a fresh UID.

type UserPasskey

type UserPasskey struct {
	UID               string     `bun:"uid,pk,type:varchar(36)"`
	UserUID           string     `bun:"user_uid,notnull"`
	Name              string     `bun:"name,notnull"`
	CredentialID      []byte     `bun:"credential_id,notnull"`
	PublicKey         []byte     `bun:"public_key,notnull"`
	AAGUID            *string    `bun:"aaguid"`
	SignCount         uint32     `bun:"sign_count,notnull"`
	Transports        []string   `bun:"transports,type:jsonb,nullzero"`
	BackupEligible    bool       `bun:"backup_eligible,notnull"`
	BackupState       bool       `bun:"backup_state,notnull"`
	UserVerified      bool       `bun:"user_verified,notnull"`
	AttestationFormat *string    `bun:"attestation_format"`
	LastUsedAt        *time.Time `bun:"last_used_at"`
	CreatedAt         time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt         time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt         *time.Time `bun:"deleted_at"`

	User *User `bun:"rel:belongs-to,join:user_uid=uid"`
}

UserPasskey is a registered WebAuthn credential. The public key is not a secret, so no encryption-at-rest envelope is needed. SignCount is a monotonically-increasing replay guard reported by the authenticator; regressions indicate a cloned credential and should reject the assertion.

func NewUserPasskey

func NewUserPasskey(userUID, name string, credentialID, publicKey []byte) *UserPasskey

NewUserPasskey builds a new passkey row with a generated UID.

type UserPasskeyUpdate

type UserPasskeyUpdate struct {
	Name        *string
	SignCount   *uint32
	LastUsedAt  *time.Time
	BackupState *bool
}

UserPasskeyUpdate carries the mutable subset of UserPasskey. SignCount and LastUsedAt update on every successful assertion; Name updates via the rename endpoint.

type UserProvider

type UserProvider struct {
	UID          string       `bun:"uid,pk,type:varchar(36)"`
	UserUID      string       `bun:"user_uid,notnull"`
	ProviderType ProviderType `bun:"provider_type,notnull"`
	ProviderID   string       `bun:"provider_id,notnull"`
	Metadata     JSONMap      `bun:"metadata,type:jsonb,nullzero"`
	CreatedAt    time.Time    `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt    time.Time    `bun:"updated_at,notnull,default:current_timestamp"`

	// Relations (for eager loading)
	User *User `bun:"rel:belongs-to,join:user_uid=uid"`
}

UserProvider links a user to an external auth provider.

func NewUserProvider

func NewUserProvider(userUID string, providerType ProviderType, providerID string) *UserProvider

NewUserProvider creates a new user provider with generated UID.

type UserToken

type UserToken struct {
	UID             string     `bun:"uid,pk,type:varchar(36)"`
	UserUID         string     `bun:"user_uid,notnull"`
	OrganizationUID *string    `bun:"organization_uid"`
	Token           string     `bun:"token,notnull"`
	Type            TokenType  `bun:"type,notnull"`
	Properties      JSONMap    `bun:"properties,type:jsonb,nullzero"`
	ExpiresAt       *time.Time `bun:"expires_at"`
	LastActiveAt    *time.Time `bun:"last_active_at"`
	CreatedAt       time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt       time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt       *time.Time `bun:"deleted_at"`

	// Relations (for eager loading)
	User         *User         `bun:"rel:belongs-to,join:user_uid=uid"`
	Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}

UserToken represents an authentication token (PAT, session refresh token, or OAuth refresh grant).

func NewUserToken

func NewUserToken(userUID string, orgUID *string, token string, tokenType TokenType) *UserToken

NewUserToken creates a new user token with generated UID. orgUID can be nil for global refresh tokens.

type UserTokenUpdate

type UserTokenUpdate struct {
	Properties   *JSONMap
	ExpiresAt    *time.Time
	LastActiveAt *time.Time
}

UserTokenUpdate represents fields that can be updated.

type UserUpdate

type UserUpdate struct {
	Email             *string
	Name              *string
	AvatarURL         *string
	PasswordHash      *string
	EmailVerifiedAt   *time.Time
	SuperAdmin        *bool
	TOTPSecret        *string
	TOTPEnabled       *bool
	TOTPRecoveryCodes *[]string
	// MustChangePassword sets or clears the forced-rotation flag. Nil leaves it
	// alone — so an unrelated profile update can never silently un-force a
	// pending rotation.
	MustChangePassword *bool
	// Demo sets or clears the shared-demo-principal flag. Nil leaves it alone.
	Demo         *bool
	LastActiveAt *time.Time
}

UserUpdate represents fields that can be updated.

type Worker

type Worker struct {
	UID          string     `bun:"uid,pk,type:varchar(36)"`
	Slug         string     `bun:"slug,notnull"`
	Name         string     `bun:"name,notnull"`
	Region       *string    `bun:"region"`
	LastActiveAt *time.Time `bun:"last_active_at"`
	// Capabilities is the worker's self-reported capability set — the names of
	// the things it CAN do — refreshed alongside last_active_at (specs
	// 2026-08-15-11, 2026-08-16-02). One generic set rather than a column per
	// capability, so the next capability is a pure string addition.
	//
	// THREE STATES, NOT TWO, AND NIL IS THE ONLY UNKNOWN:
	//
	//	nil               unknown — nothing was ever reported (a worker that
	//	                  predates the feature, or has not checked in yet)
	//	[]string{}        reported, and this worker has none of them
	//	[]string{"ipv6"}  reported this exact set
	//
	// A NON-NIL SET IS AUTHORITATIVE AND CLOSED: absence from it means "no",
	// never "unknown". Conflating the two is precisely the lie this feature
	// exists to stop telling — it would paint every worker predating the
	// capability report as IPv6-incapable. Read it through Capability() rather
	// than by hand so the unknown case cannot be forgotten.
	//
	// The bun tag mirrors Check.Regions: pgdialect encodes it as the `text[]`
	// the Postgres schema declares, sqlitedialect ignores `array` and stores
	// the JSON array the SQLite schema expects. `nullzero` is what maps a nil
	// slice to SQL NULL; a non-nil empty slice is NOT zero for bun (its zero
	// checker for slices is "is nil"), so it still writes `{}` / `[]`.
	Capabilities []string `bun:"capabilities,type:text[],array,nullzero"`
	// Version is the worker's self-reported build version (specs
	// 2026-08-19-07), refreshed alongside Capabilities. UNLIKE Capabilities,
	// this is a TWO-state field, not three: a real build version is never
	// the empty string, so there is no meaningful "reported, and has none"
	// answer for a scalar the way there is for a set.
	//
	//	nil       unknown — nothing was ever reported (a worker that predates
	//	          the feature, or has not sent a claim frame yet)
	//	&"x.y.z"  the version this worker last reported
	//
	// nil is the only unknown, and it must never be rendered as "drifted" —
	// an old agent that predates version reporting must not look broken. The
	// match/drifted/unknown comparison against the server's own version is
	// computed at read time (handlers/agents), not stored here.
	Version   *string    `bun:"version"`
	CreatedAt time.Time  `bun:"created_at,notnull,default:current_timestamp"`
	UpdatedAt time.Time  `bun:"updated_at,notnull,default:current_timestamp"`
	DeletedAt *time.Time `bun:"deleted_at"`
}

Worker represents a distributed worker that executes checks.

func NewWorker

func NewWorker(slug, name string) *Worker

NewWorker creates a new worker with generated UID.

func (*Worker) Capability

func (w *Worker) Capability(name string) CapabilityState

Capability answers, as a tri-state, whether this worker reports capability name. A nil set is unknown; a non-nil set is closed.

type WorkerUpdate

type WorkerUpdate struct {
	Slug         *string
	Name         *string
	Region       *string
	LastActiveAt *time.Time
}

WorkerUpdate represents fields that can be updated.

Jump to

Keyboard shortcuts

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