package store // Registry represents a container image registry. type Registry struct { ID string `json:"id"` Name string `json:"name"` URL string `json:"url"` Type string `json:"type"` Token string `json:"token"` Owner string `json:"owner"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // Settings holds global application configuration (single-row pattern). type Settings struct { Domain string `json:"domain"` ServerIP string `json:"server_ip"` // Docker host IP (for NPM remote forwarding) PublicIP string `json:"public_ip"` // Public-facing IP for DNS A records (e.g., NPM/proxy host) Network string `json:"network"` SubdomainPattern string `json:"subdomain_pattern"` NotificationURL string `json:"notification_url"` NotificationSecret string `json:"-"` // outgoing-webhook signing secret; never serialized directly NpmURL string `json:"npm_url"` NpmEmail string `json:"npm_email"` NpmPassword string `json:"npm_password"` PollingInterval string `json:"polling_interval"` BaseVolumePath string `json:"base_volume_path"` SSLCertificateID int `json:"ssl_certificate_id"` StaleThresholdDays int `json:"stale_threshold_days"` AllowedVolumePaths string `json:"allowed_volume_paths"` // JSON array of allowed absolute paths WildcardDNS bool `json:"wildcard_dns"` DNSProvider string `json:"dns_provider"` CloudflareAPIToken string `json:"cloudflare_api_token"` CloudflareZoneID string `json:"cloudflare_zone_id"` NpmRemote bool `json:"npm_remote"` NpmAccessListID int `json:"npm_access_list_id"` ProxyProvider string `json:"proxy_provider"` TraefikEntrypoint string `json:"traefik_entrypoint"` TraefikCertResolver string `json:"traefik_cert_resolver"` TraefikNetwork string `json:"traefik_network"` TraefikAPIURL string `json:"traefik_api_url"` ImagePruneThresholdMB int `json:"image_prune_threshold_mb"` BackupEnabled bool `json:"backup_enabled"` BackupIntervalHours int `json:"backup_interval_hours"` BackupRetentionCount int `json:"backup_retention_count"` // AutoBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB backup // at the start of every project deploy. Independent of BackupEnabled // (which governs the periodic auto-backup cron). AutoBackupBeforeDeploy bool `json:"auto_backup_before_deploy"` StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection UpdatedAt string `json:"updated_at"` } // ContainerStatsSample is one persisted sample of container resource usage. // Cumulative counters (network, block I/O) require differencing two samples // to get rates; CPU is already a percent-since-previous-sample value. type ContainerStatsSample struct { ContainerID string `json:"container_id"` OwnerType string `json:"owner_type"` // "instance" or "site" OwnerID string `json:"owner_id"` TS int64 `json:"ts"` // Unix seconds UTC CPUPercent float64 `json:"cpu_percent"` MemoryUsage int64 `json:"memory_usage"` MemoryLimit int64 `json:"memory_limit"` NetworkRxBytes int64 `json:"network_rx_bytes"` NetworkTxBytes int64 `json:"network_tx_bytes"` BlockReadBytes int64 `json:"block_read_bytes"` BlockWriteBytes int64 `json:"block_write_bytes"` } // SystemStatsSample is one persisted host-level snapshot that aggregates // workload usage across all containers plus daemon capacity + disk totals. type SystemStatsSample struct { TS int64 `json:"ts"` // Unix seconds UTC NCPU int `json:"ncpu"` MemoryTotal int64 `json:"memory_total"` WorkloadCPUPercent float64 `json:"workload_cpu_percent"` WorkloadMemUsage int64 `json:"workload_mem_usage"` ContainersRunning int `json:"containers_running"` DiskTotalBytes int64 `json:"disk_total_bytes"` } // Backup represents a backup metadata record. type Backup struct { ID string `json:"id"` Filename string `json:"filename"` SizeBytes int64 `json:"size_bytes"` BackupType string `json:"backup_type"` // "manual" or "auto" CreatedAt string `json:"created_at"` } // DNSRecord tracks a DNS record managed by the application. type DNSRecord struct { ID string `json:"id"` FQDN string `json:"fqdn"` ProviderRecordID string `json:"provider_record_id"` ConsumerType string `json:"consumer_type"` // "instance" or "standalone" ConsumerID string `json:"consumer_id"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // ProxyRoute shapes one proxy-enabled container row for the Proxies // page. The legacy field names (ProjectID, ProjectName, StageID, // StageName, InstanceID) are retained verbatim for the existing // frontend contract — after the workload-first cutover they map to: // // ProjectID/Name → workload id / workload name // StageID/Name → containers.stage_id / containers.role // InstanceID → container row id // Source → "instance" for image/compose, "static_site" for static // // Renaming would require a coordinated frontend change; deferred. type ProxyRoute struct { Source string `json:"source"` InstanceID string `json:"instance_id"` ProjectID string `json:"project_id"` ProjectName string `json:"project_name"` StageID string `json:"stage_id"` StageName string `json:"stage_name"` ImageTag string `json:"image_tag"` Subdomain string `json:"subdomain"` Domain string `json:"domain"` ContainerID string `json:"container_id"` Port int `json:"port"` ProxyRouteID string `json:"proxy_route_id"` NpmProxyID int `json:"npm_proxy_id"` Status string `json:"status"` CreatedAt string `json:"created_at"` } // WorkloadVolume is the plugin-shape equivalent of legacy Volume: a // per-workload mount declaration. The Scope enum matches the existing // VolumeScope contract so the legacy resolver can be reused once its // project_id assumption is loosened. type WorkloadVolume struct { ID string `json:"id"` WorkloadID string `json:"workload_id"` Source string `json:"source"` Target string `json:"target"` Scope string `json:"scope"` Name string `json:"name"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // WorkloadEnv is the plugin-shape equivalent of StageEnv: per-workload // environment variable overrides, optionally encrypted at rest. Read by // the Source plugin at deploy time, merged on top of source_config.env. type WorkloadEnv struct { ID string `json:"id"` WorkloadID string `json:"workload_id"` Key string `json:"key"` Value string `json:"value"` Encrypted bool `json:"encrypted"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // SharedSecret is an env var shared across workloads by scope. Resolved // into a workload's container env as a low-precedence default (overridden // by image cfg.Env and workload_env). type SharedSecret struct { ID string `json:"id"` Name string `json:"name"` // the env KEY Value string `json:"value"` // ciphertext when Encrypted; never returned decrypted by the API Encrypted bool `json:"encrypted"` Scope string `json:"scope"` // global | app AppID string `json:"app_id"` // set when scope == app; "" for global Description string `json:"description"` Enabled bool `json:"enabled"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // Shared-secret scope enum: a secret is either applied to every workload // (global) or only to workloads whose app_id matches (app). const ( SharedSecretScopeGlobal = "global" SharedSecretScopeApp = "app" ) // VolumeScope defines the sharing scope for a volume mount. // Valid scopes: instance, stage, project, project_named, named, ephemeral. type VolumeScope string const ( VolumeScopeInstance VolumeScope = "instance" VolumeScopeStage VolumeScope = "stage" VolumeScopeProject VolumeScope = "project" VolumeScopeProjectNamed VolumeScope = "project_named" VolumeScopeNamed VolumeScope = "named" VolumeScopeEphemeral VolumeScope = "ephemeral" VolumeScopeAbsolute VolumeScope = "absolute" ) // ValidVolumeScopes contains all valid scope values for validation. var ValidVolumeScopes = []VolumeScope{ VolumeScopeInstance, VolumeScopeStage, VolumeScopeProject, VolumeScopeProjectNamed, VolumeScopeNamed, VolumeScopeEphemeral, VolumeScopeAbsolute, } // IsValidVolumeScope returns true if the given string is a valid scope. func IsValidVolumeScope(s string) bool { for _, v := range ValidVolumeScopes { if string(v) == s { return true } } return false } // EventLog represents a persistent event log entry. type EventLog struct { ID int64 `json:"id"` Source string `json:"source"` WorkloadID string `json:"workload_id"` // "" = unscoped (non-deploy events) Severity string `json:"severity"` // info, warn, error Message string `json:"message"` Metadata string `json:"metadata"` // JSON-encoded structured data CreatedAt string `json:"created_at"` } // EventTrigger is a filter+action rule evaluated against EventLog // entries published on the bus. When all non-empty filters match, the // trigger fires its configured action (webhook today, additional action // types extensible via the ActionType enum). // // Filter fields use a comma-separated list shape for multi-value // filters (severity, source) to keep the schema flat — empty string // means "no filter on this dimension." FilterMessageRegex is a single // regex evaluated against EventLog.Message. // // Loop-prevention: deliveries are recorded in webhook_deliveries (the // existing audit trail). The dispatcher MUST NOT write to event_log // or it will recurse. type EventTrigger struct { ID int64 `json:"id"` Name string `json:"name"` FilterSeverity string `json:"filter_severity"` // comma list: "warn,error"; "" = any FilterSource string `json:"filter_source"` // comma list: "logscan,deploy"; "" = any FilterMessageRegex string `json:"filter_message_regex"` // "" = any ActionType string `json:"action_type"` // "webhook" today ActionTarget string `json:"action_target"` // URL for webhook ActionSecret string `json:"action_secret"` // optional HMAC secret for signed delivery Enabled bool `json:"enabled"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // EventTriggerActionType enumerates the supported action_type values. // Adding a new action is additive — old triggers keep working, the // dispatcher just learns a new branch. const ( EventTriggerActionWebhook = "webhook" ) // LogScanRule is one regex-based pattern the log scanner evaluates // against container log lines. The (workload_id, overrides_id) pair // implements the "global rule with optional per-workload override" // pattern documented in docs/LOGSCAN_AND_TRIGGERS_TODO.md: // // - WorkloadID == "" && OverridesID == 0 → global rule, applies to // every workload unless overridden. // - WorkloadID != "" && OverridesID == 0 → workload-only addition. // - WorkloadID != "" && OverridesID != 0 → override of the named // global rule for one workload (Enabled=false to disable globally // for this workload). type LogScanRule struct { ID int64 `json:"id"` WorkloadID string `json:"workload_id"` // "" = global OverridesID int64 `json:"overrides_id"` // 0 = not an override Name string `json:"name"` Pattern string `json:"pattern"` // regex, compiled at load Severity string `json:"severity"` // info|warn|error Streams string `json:"streams"` // all|stdout|stderr CooldownSeconds int `json:"cooldown_seconds"` Enabled bool `json:"enabled"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // Log scan stream filter values. "all" reads both streams; "stdout" // or "stderr" filter to one. Used both for store validation and at // docker-side log read time. const ( LogScanStreamAll = "all" LogScanStreamStdout = "stdout" LogScanStreamStderr = "stderr" ) // Log scan severity values mirror the event_log enum so a matched // rule lands as an event_log row with the rule's severity verbatim. const ( LogScanSeverityInfo = "info" LogScanSeverityWarn = "warn" LogScanSeverityError = "error" ) // MetricAlertRule fires an event when a container metric breaches a // threshold. Mirrors LogScanRule but evaluated against stats_samples // instead of log lines. type MetricAlertRule struct { ID int64 `json:"id"` WorkloadID string `json:"workload_id"` // "" = applies to all workloads Name string `json:"name"` Metric string `json:"metric"` // cpu_percent | memory_percent | memory_bytes Comparator string `json:"comparator"` // gt | lt Threshold float64 `json:"threshold"` Severity string `json:"severity"` // info | warn | error CooldownSeconds int `json:"cooldown_seconds"` // min seconds between fires per (rule,workload) Enabled bool `json:"enabled"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // Metric-alert metric identifiers. cpu_percent + memory_percent are // 0–100 ratios; memory_bytes is an absolute usage figure. Validated in // the store on create/update. const ( MetricCPUPercent = "cpu_percent" MetricMemoryPercent = "memory_percent" MetricMemoryBytes = "memory_bytes" ) // Metric-alert comparators. gt fires when the value exceeds the // threshold; lt when it falls below. const ( MetricComparatorGT = "gt" MetricComparatorLT = "lt" ) // WorkloadKind enumerates the legacy discriminator values written into // containers.workload_kind and workloads.kind. After the hard cutover the // backing project / stack / static_site tables are gone — these constants // are just strings used to filter the unified containers index in the UI. // // `build` is the dockerfile-source kind: a container built from a // Dockerfile in a Git repo. Operationally it looks like a site (one // container, one optional public face) but its origin is the build // pipeline, not a static-asset extract. Dashboard filters that need to // distinguish "I built this from source" from "I served files from a // repo" should key on this value. type WorkloadKind string const ( WorkloadKindProject WorkloadKind = "project" WorkloadKindStack WorkloadKind = "stack" WorkloadKindSite WorkloadKind = "site" WorkloadKindBuild WorkloadKind = "build" ) // Workload is the unifying primitive that abstracts Project, Stack, and StaticSite. // Each row is paired with exactly one project/stack/site via (Kind, RefID). // Notification + webhook config moves here so it lives in one place across kinds. // // SourceKind / SourceConfig / TriggerKind / TriggerConfig / PublicFaces / // ParentWorkloadID populate the unified plugin model from the Workload-first // refactor. Existing rows keep these empty until they are explicitly migrated // or replaced — the legacy Kind/RefID columns continue to point at // project/stack/site rows in parallel during the cutover. type Workload struct { ID string `json:"id"` Kind string `json:"kind"` // project | stack | site (legacy discriminator) RefID string `json:"ref_id"` Name string `json:"name"` AppID string `json:"app_id"` // nullable; "" = unassigned (a.k.a. GroupID after rename) SourceKind string `json:"source_kind"` // "" until plugin-mode populated SourceConfig string `json:"source_config"` // JSON-encoded, decoded by the matching Source TriggerKind string `json:"trigger_kind"` TriggerConfig string `json:"trigger_config"` // JSON-encoded, decoded by the matching Trigger PublicFaces string `json:"public_faces"` // JSON-encoded []PublicFace ParentWorkloadID string `json:"parent_workload_id"` // "" = root; non-empty = stage chain NotificationURL string `json:"notification_url"` NotificationSecret string `json:"-"` // never serialized WebhookSecret string `json:"-"` // URL-identifier secret; never serialized WebhookSigningSecret string `json:"-"` // HMAC key; never serialized WebhookRequireSignature bool `json:"webhook_require_signature"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // WorkloadNotification is one configured outbound notification route for // a workload. Multiple rows per workload model the "one Slack channel // for failures, one Discord webhook for successes" routing the legacy // single notification_url column could not express. // // EventTypes is a comma-separated allow-list (e.g. "build_failure" or // "deploy_success,deploy_failure"). An empty EventTypes means the row // fires for every event type — the cheapest way to keep the existing // single-destination behaviour expressible in the new shape. // // Secret round-trips through the same crypto envelope as other stored // secrets; the API layer strips it from responses. type WorkloadNotification struct { ID string `json:"id"` WorkloadID string `json:"workload_id"` Name string `json:"name"` URL string `json:"url"` Secret string `json:"-"` EventTypes string `json:"event_types"` Enabled bool `json:"enabled"` SortOrder int `json:"sort_order"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // Container is the normalized index of every Tinyforge-managed container. // Replaces the project-specific Instance table after migration. Subdomain/ // proxy fields are hoisted as first-class columns because ListProxyRoutes, // stale detection, and dashboard queries filter on them frequently. // // StageID is populated by the deployer for project containers so ListProxyRoutes // survives stage renames; it stays empty for stack and site rows. type Container struct { ID string `json:"id"` WorkloadID string `json:"workload_id"` WorkloadKind string `json:"workload_kind"` // denormalized for filtered queries Role string `json:"role"` // stage name (project), service name (stack), '' (site) StageID string `json:"stage_id"` // project containers only; '' otherwise ContainerID string `json:"container_id"` // Docker container ID; '' between create+start ImageRef string `json:"image_ref"` // "image:tag" as scheduled ImageTag string `json:"image_tag"` // just the tag, for ListProxyRoutes Host string `json:"host"` State string `json:"state"` // running | stopped | failed | removing | missing Port int `json:"port"` Subdomain string `json:"subdomain"` ProxyRouteID string `json:"proxy_route_id"` NpmProxyID int `json:"npm_proxy_id"` LastSeenAt string `json:"last_seen_at"` // ExtraJSON carries source-specific metadata that isn't promoted to a // first-class column — currently per-face proxy route IDs for // multi-face image deploys. Stored as a JSON object; '{}' on empty // rows. Sources own the shape; consumers should tolerate unknown // keys. ExtraJSON string `json:"extra_json"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // Trigger is a first-class redeploy signal source. Triggers were embedded // in workload rows (workload.trigger_kind / trigger_config) until the // trigger-split refactor; they are now standalone records bound to // workloads via WorkloadTriggerBinding so a single trigger (a webhook, // registry watcher, schedule, git push) can fan out to many workloads. // // Webhook secrets live here, not on the workload — the inbound webhook // URL identifies a trigger, which then resolves its bindings to decide // which workloads to fire. type Trigger struct { ID string `json:"id"` Kind string `json:"kind"` // registry | git | manual | schedule | log_scan | ... Name string `json:"name"` // human-readable, unique Config string `json:"config"` // JSON-encoded, decoded by the matching plugin WebhookSecret string `json:"-"` // URL-identifier secret; never serialized WebhookSigningSecret string `json:"-"` // HMAC key; never serialized WebhookRequireSignature bool `json:"webhook_require_signature"` // LastFiredAt is the RFC3339 wall-clock the scheduler last dispatched // this trigger. Empty for never-fired or non-schedule triggers. The // scheduler reads + writes this column to decide next-fire windows // and to surface "last fired" on the trigger detail page. LastFiredAt string `json:"last_fired_at,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // WorkloadTriggerBinding joins a Workload to a Trigger. BindingConfig is // the per-binding override applied on top of Trigger.Config (top-level // JSON merge: binding fields win). Empty BindingConfig means "use the // trigger's config verbatim". Enabled false skips the binding without // deleting it (useful for paused stages). type WorkloadTriggerBinding struct { ID string `json:"id"` WorkloadID string `json:"workload_id"` TriggerID string `json:"trigger_id"` BindingConfig string `json:"binding_config"` // JSON-encoded; "{}" = none Enabled bool `json:"enabled"` SortOrder int `json:"sort_order"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // App is an optional grouping of workloads (e.g., "my-saas" = web project + worker stack + redis stack). // Schema lives here from day one so future UI work is unblocked, but no UI is wired in v1. type App struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` }