WiseHosting
Reference

Data layer

GORM wrapper, model definitions, AES-GCM encryption, naming, config, plans.

Files: internal/database/*, internal/models/models.go, internal/config/config.go, internal/plans/plans.go.

internal/database/gorm_db.go

Thin wrapper around *gorm.DB with an in-memory TTL cache (sync.Map), optional AES-GCM encryption for sensitive fields, and all application-level data-access methods. Establishes the Postgres connection (max 25 open / 5 idle, 5-min lifetime), runs RunMigrations on startup (see migrate.go), exposes ErrConflict and ErrAccountExists.

  • Connect(dsn) (*GormDB, error) — Opens, pings, configures pool, runs RunMigrations(db). Side effect: DDL applied via golang-migrate from the embedded migrations/*.sql set. Refuses to start on a dirty schema_migrations row.
  • (db) EnableSecretEncryption(serverSecret) error — Derives AES-GCM key from secret.
  • (db) DecryptJobPayload(stored) string — Public wrapper; returns plaintext or original.
  • ValidEnvKey(k) bool^[A-Z][A-Z0-9_]{0,63}$.
  • (db) InvalidateUserCache(userID) / InvalidateAppCache(appID) — Cache eviction.
  • (db) ListAppEnvVars(appID) — Decrypts each value.
  • (db) SetAppEnvVar(appID, key, value) — Validates key, encrypts, upserts.
  • (db) DeleteAppEnvVar(appID, key).
  • (db) AppEnvMap(appID) — Decrypted map[string]string.
  • (db) Close() error.
  • (db) FindUserByID(id).
  • (db) UpsertGoogleUser(googleID, email, name, avatar) — ON CONFLICT on google_id.
  • (db) UpsertGitToken(userID, provider, username, plaintext) — Encrypts + upserts.
  • (db) DeleteGitToken(userID, provider).
  • (db) GetGitToken(userID, provider) (token, username, ok) — Decrypts.
  • (db) ListGitTokens(userID) []LinkedProvider.
  • (db) MarkWorkerOnline(workerID) / ListWorkers() / FindWorkerByID(id).
  • (db) FindOnlineWorkerByAPIKey(rawKey) — Hashes rawKey first, queries on api_key_hash. 30 s cache (keyed by hash). Despite the name, matches workers regardless of status so an offline worker can still reconnect (see wsproto).
  • (db) ListAppsByWorker(workerID) — Apps currently scheduled on this worker. Used by /v1/traefik/config to emit per-app routers for verified custom domains.
  • HashAPIKey(rawKey) string / HashAPIKeyBytes(rawKey) []byte — Public helpers exposing the same hash used at rest. Bytes form is the WSS HMAC signing key (recovered server-side from api_key_hash, derived independently on the worker side).
  • (db) UpsertWorker(name, ip, rawAPIKey, zone, regionName, capCPU, capMem) — Stores sha256(rawAPIKey) as api_key_hash; raw key never persisted.
  • (db) UpdateWorkerHeartbeat(workerID, status, res).
  • (db) FindAppByUserIDAndName(userID, appName) / FindAppByID(appID) — Cache-first.
  • (db) ListUserApps(userID) — 2-min cache.
  • (db) MarkAppDeleted(appID) / HardDeleteApp(appID) — The hard variant cancels pending/assigned jobs in a tx.
  • (db) MarkAppStopped(appID) / MarkAppViolated(appID, reason).
  • (db) FindDeployment(id) / ListDeployments(appID, limit).
  • (db) ListRecentUserDeployments(userID, limit) — Raw SQL join across apps for non-deleted apps.
  • (db) LatestBuildLogs(appID) — From the most recent deployment.
  • (db) SetDeploymentBuildLogs(deploymentID, workerID, logs) — Verifies worker owns deployment, strips null bytes.
  • (db) FindJobForWorker(jobID, workerID) / NextAssignedJob(workerID) — Decrypts payload.
  • (db) UpdateJobStatus(jobID, workerID, status, message, progress) — Sets started_at on transition to processing.
  • (db) FindActiveJobForApp(appID) — Most recent pending/assigned/processing.
  • (db) CompleteJob(job, status, result) — Tx: marks job + updates app status + deployment fields.
  • (db) CreateDeploymentWithSpec(...) — Tx: unique app name/subdomain, env vars, deployment, encrypted job payload.
  • (db) SetAppWebhookID(appID, hookID) / UpdateAppSpec(appID, updates) / SetAppAutoDeploy(appID, enabled).
  • (db) FindAppByUUID(uuid).
  • (db) CreateRedeployment(appID, workerID int, payload, commitSHA, branch string) — Tx: deployment + restart job pinned, app deploying. Commit metadata is persisted on the deployment row up-front so failure paths still carry the right git identity.
  • (db) CreatePinnedJob(jobType, appID, workerID, payload, appStatus).
  • (db) CreateDomain(d) / ListDomainsForUser(userID) / ListDomainsForApp(appID) / FindDomain(id, userID).
  • (db) ListVerifiedDomainsForApp(appID) []string — Hostname-only slice; used by traefikConfig to render Host() rules.
  • (db) MarkDomainVerified(id) / MarkDomainCheckFailed(id, reason) / DeleteDomain(id, userID).
  • (db) ListWebhooks(userID) / ListWebhooksForEvent(event) / FindWebhook(id, userID) / FindWebhookByID(id).
  • (db) CreateWebhook(w) / UpdateWebhook(id, userID, updates) / DeleteWebhook(id, userID) — The latter cascades webhook_deliveries.
  • (db) RecordWebhookDelivery(d) — Inserts + updates parent counters.
  • (db) ListWebhookDeliveries(webhookID, userID, limit).
  • (db) WebhookStats(userID) (total, active, failed7d, lastAt, err).

Sessions + audit

  • (db) CreateSession(userID, tokenHash, ip, geo, ua, twoFactor bool) (sessionID, err) — Inserts a sessions row, recording whether the issue passed through 2FA.
  • (db) FindSessionByTokenHash(hash) (Session, err) — Looks up by sha256(token); honours revoked_at.
  • (db) BumpSessionLastSeen(sessionID, ip, ua) — Debounced (caller throttles to once/min). Drives the rolling 30-day TTL — idle sessions expire from last_seen_at, not created_at.
  • (db) ListSessionsForUser(userID) []Session — Newest first.
  • (db) RevokeSession(id, userID) / (db) RevokeOtherSessions(userID, exceptID).
  • (db) PruneRevokedSessions(maxAge) error — Hard-deletes revoked or 30+ day-old rows. Called every 6 hours from main.go.
  • (db) RecordAuditEvent(e) — Append-only insert into audit_events. Replaces the old RecordLoginEvent after the table was renamed in migration 0005.
  • (db) ListAuditEvents(userID, limit) []AuditEvent.
  • (db) HasAuditEventForIP(userID, ip) bool — Drives the account.login_new_device webhook (formerly HasLoginEventForIP).

Audit kinds emitted by the web layer: login, logout, session_revoked, app_deploy, app_restart, app_start, app_stop, app_delete, env_set, env_delete, git_token_revoke, domain_create, domain_verify, domain_delete.

2FA / TOTP

  • (db) SetTOTPSecret(userID, encSecret) / (db) GetTOTPSecret(userID) — Stores and reads the AES-GCM-encrypted shared secret on users.
  • (db) EnableTOTP(userID) / (db) DisableTOTP(userID) — Flip totp_enabled, stamp totp_verified_at, clear backup codes on disable.
  • (db) ReplaceBackupCodes(userID, hashes []string) — Bulk-replace argon2 hashes in backup_codes; called on first verify and re-generate.
  • (db) ConsumeBackupCode(userID, plaintext) (ok bool, err) — Argon2-compares against unused rows; sets used_at on hit.
  • (db) CreatePendingTOTP(userID, provider, ttl) (token string, err) — Inserts a pending_totps row keyed by an opaque token (the value of the wh_2fa_pending cookie).
  • (db) ConsumePendingTOTP(token) (userID, provider, err) — Atomically deletes and returns the row; rejects expired entries.
  • (db) PrunePendingTOTPs() error — Bulk-delete expired challenges. Called every 6 hours from main.go.

Alerts

  • (db) EnsureAppAlertRules(appID, userID) — UPSERTs the default rule set if missing.
  • (db) ListAppAlertRules(appID) []AppAlertRule.
  • (db) UpsertAppAlertRule(rule).
  • (db) ListEnabledAlertRules() []AppAlertRule — Used by the threshold poller.
  • (db) ListAppsForRules() []App — Running apps with their owner.
  • (db) CreateAlert(a) / (db) FindActiveAlertBySource(userID, srcType, srcID) (Alert, ok).
  • (db) ResolveAlertsBySource(userID, srcType, srcID).
  • (db) ListAlerts(userID, filter) ([]Alert, AlertStats) — Filter by status/kind/app, returns the headline counters used by the dashboard.
  • (db) UpdateAlertStatus(id, userID, status).

Usage time-series

  • (db) UpsertUsageSample(s) — Inserts/updates by composite PK (app_id, bucket_start).
  • (db) ListUsageSamples(scope, from, to) []UsageSample — Bounded by 90-day retention.
  • (db) PruneUsageSamples(olderThan time.Time) int64 — Daily pruner.

internal/database/migrate.go

Thin wrapper around golang-migrate that lets the binary ship its own SQL.

  • RunMigrations(db) error — Builds an iofs source from migrations/*.sql (embedded via embed.FS), wraps the live *sql.DB in golang-migrate/database/postgres, and calls Up(). No-op when current. Returns a structured error on a dirty schema_migrations row so the caller can fail fast.

Schema changes ship as new 000N_<slug>.up.sql / 000N_<slug>.down.sql pairs in internal/database/migrations/. There is no GORM AutoMigrate path anymore — model struct changes that aren't backed by a migration will pass go build but fail at runtime when GORM scans columns.


internal/database/models.go

GORM model definitions with explicit TableName() methods and BeforeCreate/BeforeUpdate hooks on User. See Architecture → Database schema for full column tables.

Models: User, Worker, App, Deployment, Job, AppEnvVar, GitProviderToken, Webhook, WebhookDelivery, Session, AuditEvent, AppAlertRule, Alert, UsageSample, BackupCode, PendingTOTP, Domain.

Notable column-level details: Worker.APIKeyHash (column api_key_hash, sha256 hex) replaces the former APIKey. Job no longer has MaxRetries / RetryCount (dropped in migration 0004).

  • All (<Model>) TableName() string — Explicit table name.
  • (u *User) BeforeCreate(tx) error — Stamps CreatedAt/UpdatedAt if zero.
  • (u *User) BeforeUpdate(tx) error — Sets UpdatedAt = now.

internal/database/naming.go

  • ValidateAppNameInput(s) error — Length 7-32, charset [a-z0-9-], not in reserved blocklist.
  • NewAppUUID() string — UUID v4 from crypto/rand.
  • GenerateAppName() stringadjective-noun-NNNN from two 64-word lists via math/rand/v2.

internal/database/secret.go

AES-256-GCM authenticated encryption for sensitive strings. Keys are derived from api_server.secret via HKDF-SHA256 with a per-purpose info label, so different uses (column AES, OAuth state, JWT signing) get independent keys.

  • deriveKey(masterSecret, purpose) []byte — HKDF-SHA256, 32-byte output.
  • newSecretCipher(serverSecret) (*secretCipher, error) — Uses purpose wisehosting-aes-v1.
  • (s) Encrypt(plain) (stored, err) — Returns v1:<base64url-no-pad>; empty in → empty out.
  • (s) Decrypt(stored) (plain, err) — Pass-through if the v1: prefix is absent (supports zero-downtime rollout and key rotation).

Type and constructor are package-private; surfaced via GormDB helpers above.


internal/models/models.go

Pure Go structs used for inter-service messaging. No GORM, no DB.

  • DeployPayload — Build/run parameters.
  • ViolationReport, AppStatusReport.
  • ResourceLimits — CPU/memory/PIDs/disk/bandwidth.
  • JobResult — Container ID, port, logs, commit metadata, error.
  • HeartbeatRequest, WorkerResources, JobStatusUpdate.

internal/config/config.go

Loads YAML from $CONFIG_PATH (default config.yaml); typed accessor methods for every field. See Configuration for the full field catalogue.

  • Load() (*Config, error) — Reads file, then overlays any systemd-supplied credentials (see below), then Validate.
  • loadCredential(name) (string, bool) — Reads $CREDENTIALS_DIRECTORY/<name> populated by systemd LoadCredential=. The post-parse overlay wins over file values for: api_server_secret, database_password, google_oauth_client_secret, github_oauth_client_secret, gitlab_oauth_client_secret, bitbucket_oauth_client_secret, codeberg_oauth_client_secret. Lets config.yaml ship without secrets.
  • (c) Validate() error — Required: database.password, api_server.secret (≥16 chars), container.port_range_*, platform.domain.
  • (c) DatabaseDSN() / APIServerAddr() / APIServerSecret() / APIServerTLS() / APIServerBaseURL().
  • (c) JobPollInterval() (5 s) / JobBuildTimeout() (10 m) / JobContainerStartTimeout() (30 s) / APIRateWindow() (1 m) — All with parse fallbacks.
  • One accessor per field for WebApp URL, all 5 OAuth providers (Google + 4 git providers), platform, worker, container, security, logging.

internal/plans/plans.go

Static plan registry; runtime overrides via YAML.

PlanMaxAppsCPUMemoryDiskBandwidth
free11512 MiB1 GiB10mbit
pro522 GiB10 GiB100mbit
business0 (unlimited)48 GiB50 GiBunlimited
  • For(id) Plan — Returns plan by ID; falls back to free.
  • All() []Plan[free, pro, business].
  • Apply(overrides) — Mutates the global registry.

On this page