HA scheduler: the job scheduler and alert evaluator hold session-scoped PostgreSQL advisory locks on pinned pool connections — replicas scan for due work exactly once, failover verified live with two replicas. Manual and hook-triggered runs stay available on any replica. Email surface: mailer renders registered templates (invitation); every send is logged to core.email_log with status and error text. New GET /projects/:id/emails endpoint; Members page shows an Email log card. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
23 KiB
Changelog
All notable changes to Primora. Format follows Keep a Changelog; versions follow SemVer.
[Unreleased]
Added
- Local secrets vault in the CLI (
@primora/cli0.6.0) —primora vault init/statusandprimora secrets set|get|list|rm|importmanage an encrypted~/.config/primora/vault.bin(Argon2id 64 MiB/3/4 → XChaCha20-Poly1305, header bound as AEAD AAD, atomic writes). Entries carry value + optionalurl/notesmetadata. Password viaPRIMORA_VAULT_PASSWORD,--password-file, or hidden prompt; file path overridable withPRIMORA_VAULT. Clean-room format — no external vault compatibility. - Vault sessions —
primora vault unlock --ttl 900proves the password once and writes a TTL'dvault.session(0600); agents and scripts use the vault without the password.primora vault lockrevokes immediately. primora agentcredential broker — wraps a child process with dummy tokens and a loopback base URL; the proxy attaches the real secret on the wire per grant (TTL, cap 3600 s). Presets:anthropic,openai,openrouter,groq,deepseek,xai,github,npm;--upstream name=https://…covers arbitrary Bearer APIs. Base-URL broker, not TLS MITM — tools must honor a base-URL override.primora inject— runs a child process with vault secrets in its environment (--all, or--env-filetemplates containingprimora://NAMErefs). Nothing on disk, nothing in argv.primora stack:*operator group —up,down,status,logs [service],pull,backupwrapdocker composein--dir(default cwd).stack up --vaultmaterializes.envfrom the vault for the duration of the run (refuses to clobber an existing.envwithout--force, which swaps and restores it).stack backupwrites apg_dump -Fcof the platform database to./backups.- Vault MCP tools (
@primora/mcp) —primora_vault_status,primora_vault_list(metadata only — values are never returned), andprimora_vault_execwhich runsprimoracommands with a secret injected into the child environment;vault/secrets/inject/agent/login/logout/useandkeys:createare denied so no response can carry a credential. - Secrets Vault window in the desktop app (
@primora/desktop) — tray "Secrets Vault…" opens a dedicated window (ui/vault.html) covering init, timed unlock/lock with live countdown, secret list with copy-to-clipboard (values never displayed), add/delete, and.envimport. IPC shells out to theprimorabinary (PRIMORA_CLIoverrides), so desktop and terminal share one vault file and one crypto implementation; a missing CLI shows install guidance. Also reachable from the connect screen. - Project vault — server-side secrets (
core.project_secrets, migration 00011) — per-project secrets encrypted AES-256-GCM at rest via the existing encryptor. API:GET/PUT/DELETE /projects/:projectID/secrets[/:name]plusPOST …/reveal. Lists return metadata only (name, url, notes, timestamps); reveal decrypts explicitly and is audit-logged (secret.set/secret.revealed/secret.deleted). Scheduled-job payloads reference secrets assecret://NAME, resolved to plaintext at delivery — refs are stored, values are not; unresolved or unconfigured refs fail the run loudly. - Vault page in the dashboard — project secrets table with create/update,
.envimport, delete, and copy-to-clipboard reveal; plus a "This machine" card when running inside the desktop shell that drives the local vault through the same Tauri IPC commands as the vault window. - CLI remote mode —
primora secrets set|get|list|rm|import --remoteoperates on the project vault via the existing API auth (--projectoverrides).get --remoteis the explicit reveal path;list --remotenever returns values. - Project-vault MCP tools —
primora_secrets_list(metadata only) andprimora_secrets_set(write-only). No reveal or delete tools — agents consume secrets throughsecret://NAMEreferences in job payloads without holding plaintext. - Realtime channel subscriptions (
@primora/client) —createRealtimeClient({ endpoint, key, projectId })opens the project SSE stream (/realtime/stream) via fetch + ReadableStream (EventSource can't sendX-API-Key).channel("documents").on("created", cb)subscribes by event namespace (document/object/issue/deploy,"*"for all); auto-reconnects with capped exponential backoff;onStateChangereportsconnecting|open|reconnecting|closed. - Telemetry alert rules (
core.alert_rules, migration 00012) —heartbeat_silence(component quiet for N minutes) anderror_spike(≥N errors in a sliding window) rules per project, evaluated every 60 s. Rules fire on state transitions only:alert.firedonce when breached,alert.resolvedon recovery — both are first-class webhook event types and ride the existing signed dispatcher + realtime stream. API:GET/POST /projects/:id/alerts,PUT/DELETE …/alerts/:ruleID; dashboard manages them from Integrations → Webhooks;firingshows the breached components. - Inbound webhooks (
core.inbound_hooks, migration 00013) — public ingest endpointsPOST /api/v1/hooks/<token>(the token is the credential).eventmode republishes the body asinbound.receiveddomain events through the signed dispatcher + realtime;jobmode triggers a scheduled-job run with the received body as its payload (triggered_by: "hook"). OptionalsecretrequiresX-Primora-SignatureHMAC-SHA256 — the same scheme outbound deliveries use. Bodies capped at 256 KiB. Managed from Integrations → Webhooks. - Document filtering and ordering —
GET /collections/:id/documentsacceptsfilter(comma-separatedfield.op.value; opseq neq gt gte lt lte like in is; dotted paths index into document data,id/created_at/updated_atfilterable) andorder(field.asc|field.desclist). Whitelisted parsing + parameterized values; plain lists unchanged. CLI:primora documents list --collection <slug> --filter … --order …. - HA scheduler leadership — the job scheduler and alert evaluator each hold a session-scoped PostgreSQL advisory lock on a dedicated pinned connection, so multiple backend replicas scan for due work exactly once; manual and hook-triggered runs remain available on any replica. Leadership is released on shutdown and fails over automatically when a replica dies — verified live with a two-replica test.
- Email templates + send log (
core.email_log, migration 00014) — the mailer renders registered templates (currentlyinvitation) and every send — successful or failed — is recorded with template, recipient, subject, status, and error text.GET /projects/:id/emailslists recent sends; the Members page shows an "Email log" card under pending invitations.
[0.7.1] - 2026-09-23
Fixed
- Self-hosted sign-up no longer requires mail infrastructure —
sendTransactionalEmailwarned-and-returned when no transport was configured instead of throwing, so SMTP-less instances 500'd on sign-up.sendVerificationEmailnow also catches transport failures; a broken SMTP cannot block account creation (verification stays optional). - Password managers detect the login form — the email/password fields gained
id/nameattributes andlabel for/idpairing, and the email input usesautocomplete="username"(Proton Pass et al.). - Installer prebuilt path strips build blocks —
install.shpreviously kept*-buildcompose blocks when pulling prebuilt GHCR images.
Added
- Static landing page (
landing/) — self-contained marketing site with bundled Geist fonts and product screenshots; deployed separately from the app artifacts.
Infrastructure
- Automatic releases on merge — merges to
masterwhose rootpackage.jsonversion has no matchingv*tag now trigger the full release pipeline (GHCR images, CLI binaries, desktop bundles, npm tarballs, GitHub Release). Tag pushes still work as before.
[0.7.0] - 2026-09-21
Changed
- Public sign-up closes by default after the first account. On upgrade, existing instances keep their users but new public sign-ups are rejected until an admin enables them (Settings → Instance or
SIGNUP_ENABLED=true). The oldest existing account is not automatically promoted — setAUTH_ADMIN_EMAILSor updateuser.roletoadminmanually if no platform admin exists. - OAuth providers require
PRIMORA_MANAGED=true. Self-hosted deployments are email/password only; GitHub/Google/Discord/Microsoft env keys are ignored without the flag.
Added
- Instance administration —
core.settings(migration 00010) stores in-app overrides that resolve ahead of.envand built-in defaults. New endpoints:GET /instance/public(unauthenticated login-page state),GET/PUT/DELETE /instance/settings[/key](platform admin only — better-authuser.role, not org role). Manageable keys cover public sign-up, mail transport (Resend or SMTP), and API/user rate limits. Secrets are AES-256-GCM encrypted at rest, never returned by the API, and decryptable by the auth service via the sharedPRIMORA_ENCRYPTION_KEY. - Bootstrap + sign-up gating — the first public sign-up becomes the instance admin; afterwards public sign-up is closed unless
SIGNUP_ENABLED=trueor the admin reopens it in Settings → Instance.databaseHooks.user.create.beforegates only/sign-up/email, so admin-created users and OAuth flows bypass it. - Managed-tier OAuth gate —
PRIMORA_MANAGED=trueis now required for GitHub/Google/Discord/Microsoft sign-in; self-hosted instances are email/password only. The login page hides the sign-up tab and social buttons according toGET /instance/public. - Settings → Instance tab — platform admins edit sign-up, mail, and rate-limit settings in-app with source badges (in-app/env/default) and per-key reset.
[0.6.0] - 2026-09-21
Added
- Scheduled jobs —
core.scheduled_jobs+core.scheduled_job_runs(migration 00009). Cron expressions and descriptors (@hourly,@daily,@every 30m,*/15 * * * *) parsed via robfig/cron; an in-processJobSchedulerscans every 15 s, queues due jobs through a buffered channel, and collapses missed schedules into a single run. Endpoints under/projects/:id/jobs: list/create/update/delete,GET …/jobs/:id/runs,POST …/jobs/:id/run(manual execution). Jobs deliver a JSON envelope (event type, project/job/run IDs, trigger source, timestamp, static payload) via POST withX-Primora-Signature: sha256=<hmac-sha256(body)>,X-Primora-Event,X-Primora-Job,X-Primora-Runheaders and a 10 s timeout. Secrets are AES-256-GCM sealed at rest; a generated secret is returned exactly once on create/rotate (has_secretthereafter). Run records carry status, trigger, HTTP status, duration, error text, and timestamps; history is pruned by retention. Reads require viewer+, writes admin/developer. - Domain events + realtime stream —
PlatformService.publishEventfans project mutations out to realtime subscribers and matching webhooks through one path. New webhook event types:job.run,document.created/updated/deleted,object.created/updated/deleted.GET …/projects/:projectID/realtime/streamserves an SSE feed with 25 s keepalives and?token=/?api_key=query auth forEventSourceclients. - Automation page — SolidJS view with a Schedules tab (job table, create modal with schedule helper text, expandable run history, manual Run, enable/disable, delete, secret-shown-once banner) and a Live events tab (live/offline badge, pause, clear, auto-reconnect). Sidebar + command palette entries; demo-mode seeds and stubs included.
primora jobs:*CLI group —jobs:list,jobs:create <name> --schedule --url [--payload --secret --disabled],jobs:run <job>,jobs:runs <job> --limit,jobs:rm <job> --yes. Bothjobs listandjobs:listspellings.- New
primora_*MCP tools —primora_list_jobs,primora_create_job,primora_update_job,primora_delete_job,primora_list_job_runs,primora_run_job. Webhook event schema extended to all new event types.
Fixed
- SSE broken by gzip middleware —
Compression()wrapped everyAccept-Encoding: gzipresponse, and the gzip buffer swallowed eachFlush(), soEventSourcereceived headers then silence and closed. Paths ending in/streamnow bypass compression; this repaired the pre-existing telemetry stream too. - Event payloads serialized as base64 —
document/objectfields inside domain-event envelopes werejson.RawMessageplaced intomap[string]any, which marshals as base64. They're now wrapped so the raw document embeds in the event JSON. - Auth trusted origins ignored the configured public URL —
docker-compose.ymlhardcodedhttp://localhostforBETTER_AUTH_URL,AUTH_BASE_URL, andVITE_APP_URL, so any non-defaultNGINX_PORTor domain broke sign-in with a 403. All three now read.envwith localhost defaults. - Frontend auth base URL on non-default ports — the bundled default baked
http://localhost;auth-clientnow resolves/authagainstwindow.location.origin(absolute, as better-auth requires) whenVITE_AUTH_BASE_URLis unset.
Security
- Managed platform connections removed —
platform-postgres/platform-dragonflyseeds carried the backend's ownDATABASE_URL/DRAGONFLY_URLinto every project, where any member could run arbitrary queries through DBX againstcoretables. The seeds are gone and migration 00008 deletes existing managed rows. Operators keep directpsql/redis-cliaccess. - Connection passwords encrypted at rest —
core.db_connections.config.passwordis now AES-256-GCM sealed (enc:v1:marker) viaPRIMORA_ENCRYPTION_KEY; decrypted only when registering the connection with DBX. Migration 00008 strips pre-existing plaintext passwords. - DBX naming — connection names now prefix the full project UUID; the previous 8-hex prefix could collide across projects and alias the wrong credentials.
Infrastructure
- One-line installer —
install.sh(curl | bash) fetches the compose files, generates secrets, pulls prebuilt GHCR images, and falls back to a source-tarball build when packages aren't published. - CI image pipeline — per-app buildx matrix with GHA layer cache, container-contents smoke checks, Trivy scans,
docker compose configvalidation, andedge/sha-*GHCR publishing on master. - Fixed release image pushes — tags were rendered with the mixed-case owner (
ghcr.io/Dvorinka/...), which GHCR rejects; owner is now lowercased, unblocking tagged releases.
[0.5.0] - 2026-09-16
Added
-
Desktop app (
apps/desktop) — Tauri 2 thin shell for Linux/Windows/macOS. First run shows a connect screen asking for the deployment URL (validated, stored in~/.config/dev.tdvorak.primora/config.json); the webview then renders the same SolidJS bundle the server serves. System-tray icon (Show / Change Server / Quit), close-to-tray, single-instance. No embedded backend or database — the self-hosted model is preserved. -
PWA support —
vite-plugin-pwagenerates a Workbox service worker andmanifest.webmanifestin every frontend build. The app shell (JS/CSS/icons/index.html) is precached;/api,/auth, and/mailpitareNetworkOnlyand excluded from the SPA navigation fallback; Google Fonts assets are cached for a year. Installability icons (192/512/maskable + apple-touch-icon) rasterized from the existing SVG mark. -
Install prompt —
PwaInstallBannercapturesbeforeinstallpromptand offers an in-app install action inside the shell; dismissal persists inlocalStorage. -
Desktop release artifacts —
release.ymlgains adesktopjob (ubuntu/macos/windows matrix) producing AppImage/deb/rpm, dmg, and msi/nsis bundles, attached to the GitHub Release alongside the existing images and CLI binaries. -
Connector framework —
core.integrations(migration 00005) stores per-project external services with type, base URL,has_credentialsflag, health status/last_health_at. Credentials are AES-256-GCM encrypted at rest (PRIMORA_ENCRYPTION_KEY, hex/base64, 32 bytes; dev fallback outside production) and are never serialized to the client. Endpoints under/projects/:id/integrations: list, create, delete,POST …/test(5s health check against the base URL with stored credentials). -
Rybbit connector —
GET …/integrations/:id/analyticsproxies a self-hosted Rybbit instance server-side (/api/organizationssite discovery,/api/sites/:site/overview, time series, top pages/referrers) and returns a normalized shape. The API key never leaves the server; read-only, no tracking proxy. Site selection via?site=or the integration's configuredsite_id. -
Outbound webhooks —
core.webhooks+core.webhook_deliveries. Endpoints: list/create/update/delete under/projects/:id/webhooks,GET …/deliveries,POST …/test. Events:issue.created(new error fingerprint on ingest),deploy.marker(POST …/deploy-markers, records version/ref/environment/note in the audit log),webhook.test. HTTPS required for public targets; plain HTTP allowed for private/self-hosted sinks. Deliveries are signedX-Primora-Signature: sha256=<hmac-sha256(body)>and retried by an in-process dispatcher (buffered queue, 3 attempts, quadratic backoff, 10s timeout, crash recovery on boot). Generated secrets are returned exactly once. -
Integrations page — SolidJS view (Connectors + Webhooks tabs): connector table with status/test/inline Rybbit analytics panel, webhook table with per-endpoint delivery log, secret-shown-once banner, deploy-marker form. Demo-mode stubs included.
-
New
primora_*MCP tools —primora_list_integrations,primora_create_integration,primora_delete_integration,primora_test_integration,primora_integration_analytics,primora_list_webhooks,primora_create_webhook,primora_update_webhook,primora_delete_webhook,primora_list_webhook_deliveries,primora_test_webhook,primora_create_deploy_marker. -
Telemetry (IMS merge) —
POST /api/v1/ingestaccepts a single event or a ≤500-event batch, authenticated by existing projectprm_API keys viaX-Primora-Key(orX-API-Key);OPTIONSpreflight + permissive CORS for browser SDKs. Components auto-register on first use (frontend,backend,database,android,desktop,web,other). Event types:error,metric,log,heartbeat,event; missing fingerprints are derived from component + first message line. -
Issue grouping —
GET /projects/:id/issuesgroups errors by fingerprint (count, severity, first/last seen, latest component). -
Telemetry read APIs —
GET …/events(type/component/fingerprint filters,beforecursor),GET …/components,DELETE …/components/:id,GET …/telemetry/stats(bucketed series + component health + totals + metric names over 1h/24h/7d/30d),GET …/telemetry/metrics/series(avg/p50/p95/max per bucket). -
Live SSE stream —
GET …/telemetry/streampushes inserted events through an in-process hub (buffered subs, drop-on-slow, 25s keepalives).?api_key=/?token=query auth forEventSourceclients. -
Telemetry page — SolidJS view: health tab (totals, activity histogram, component status table), issues with drill-through to filtered events, event list with payload modal, metric series table, and an Integrate tab with SDK/cURL snippets. Live badge driven by the SSE stream; demo-mode stubs included.
-
@primora/clientSDK —createPrimoraClient({ endpoint, key, component, kind, base })withcaptureError,captureMetric,captureLog,captureEvent,heartbeat,installAuto, queued batching andkeepaliveflushes. -
New
primora_*MCP tools —primora_list_issues,primora_list_events,primora_telemetry_stats,primora_metric_series.
Fixed
- Workspace dependency pins updated to
0.3.0(@primora/api-client,@primora/shared-types) —npm ciin the frontend image was resolving the unpublished0.2.0spec from the registry.
[0.3.0] - 2026-09-16
Added
- Databases (DBX integration) — the Go backend keeps
dbx-mcpas a persistent stdio subprocess (internal/dbx, lazy start, one retry on failure) and exposes its tools as REST under/projects/:id/db-connections/*(list/create/delete/test, databases, tables, describe, schema context, SQL query, Redis command). DBX tool/subprocess failures surface as 502. - Connection registry —
core.db_connections(migration 00003). Credentials are stored server-side and never serialized to the browser (has_passwordflag only). Non-defaultssl,driver_profilesupported. - First-party connections —
platform-postgresandplatform-dragonflyauto-register on every project (derived fromDATABASE_URL/DRAGONFLY_URL) and cannot be deleted (409managed_connection). - Databases page — SolidJS view with connection list, database/schema/table browser, column inspector, SQL editor (⌘/Ctrl+Enter), result grid with row-detail modal, and a Redis console. Demo mode stubs included.
- Backend image now Debian-based —
dbx-mcprequires glibc; the Dockerfile installs the pinnedpackages-v0.4.88release with SHA256 verification.
[0.2.0] - 2026-09-15
Added
primoraCLI (apps/cli) —cac-parsed,@clack/promptsinteractive UX. Login (session or API key), org/project context,orgs,projects,buckets,objects(list/upload/download/rm),keys,audit list --follow. Every command accepts--json. Bothprimora orgs listandprimora orgs:listspellings.@primora/mcpserver (apps/mcp) — stdio MCP server exposing 14primora_*tools (whoami, orgs, projects, buckets, objects up/download/delete, API keys, audit query) over the generated client. Shares the CLI's~/.config/primora/config.jsoncredentials.- Authentication → Users panel — better-auth
adminplugin on the auth service (list/set-role/ban/unban/remove users) plus a dashboard view with search, role selects, and ban/delete actions. Bootstrap admins withAUTH_ADMIN_EMAILSin.env. - Mobile bottom navigation — Instagram-style tab bar below 1024px (Overview · Storage · Collections · Auth · Menu), safe-area aware; Menu opens the full drawer. Fixes a dead-navigation gap at 768–1023px.
- Release pipeline — semver tag → GitHub Release with changelog notes, GHCR images (
primora-backend/auth/frontend), compiledprimora/primora-mcpbinaries for Linux/macOS/Windows, npm tarballs, optionalnpm publishviaNPM_TOKENsecret.
Fixed
- Object downloads broken for all gzip-negotiating clients — the compression middleware left the handler's
Content-Length(raw size) on gzipped bodies, so undici/browsers saw truncated streams.gzipWriter.Writenow dropsContent-Length. - nginx 502s after every container restart — upstreams were resolved once at startup. nginx now resolves
backend/auth/frontend/mailpitthrough Docker's embedded DNS (127.0.0.11) per request.
Changed
- API keys now document the real
prm_<prefix>_<secret>format (README previously claimedpk_live_).
[0.1.0] - Initial commit
- Phase 0 foundation: Go/Gin backend, Better Auth service, SolidJS dashboard, Postgres + Dragonfly, nginx gateway, Docker Compose stack.