Self-hosted sign-up bounced back to the bootstrap screen for three
compounding reasons:
- better-auth's client resolves { data, error } on HTTP failure; the UI
ignored res.error and showed "Account created — signing you in", hiding
the real 403/429/500.
- trustedOrigins was a static localhost-only list. Browsing the instance
via LAN IP or custom port failed every auth POST once cookies existed.
Origins are now derived from the request's public host (forwarded by
nginx), so any origin serving the app works.
- hasMailTransport threw when core.settings was unreadable (fresh volume,
backend still migrating), so sign-up 500'd and the startup backfill
could crash-loop the service. Both now degrade to "no transport".
Verified live: LAN-style origin sign-up succeeds, missing core.settings
logs a warn instead of exiting, and the frontend shows real errors.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
34 KiB
Changelog
All notable changes to Primora. Format follows Keep a Changelog; versions follow SemVer.
[Unreleased]
Fixed
- Self-hosted sign-up works on any origin — trusted origins are now derived from the request's public host (forwarded by nginx), so browsing the instance via a LAN IP, hostname, or custom port no longer fails every auth POST with a 403. Static
VITE_APP_URL/AUTH_BASE_URLorigins andBETTER_AUTH_TRUSTED_ORIGINSstill apply. - Sign-up/sign-in failures are surfaced, not swallowed — better-auth resolves
{ data, error }instead of throwing, so a failed request previously showed "Account created — signing you in" and bounced back to the bootstrap screen. The real server error (invalid origin, rate limit, disabled sign-up) is now displayed. - Mail probe can no longer break auth —
hasMailTransportdegrades to "no transport" instead of throwing whencore.settingsis unreadable, and the no-transport startup backfill is best-effort. Previously, auth could crash-loop while the backend was still migrating on a fresh volume — every sign-up 502'd during the window.
[0.8.1] - 2026-09-28
Fixed
- SMTP-less instances no longer involve email at all — when no mail transport is configured (no Resend key, no
SMTP_HOST), new accounts are createdemailVerified=true, the verification send is skipped entirely, and existing unverified users are marked verified on startup..env.exampleno longer defaultsSMTP_HOSTto the dev-onlymailpitservice, which left production deploys pointing at a transport that does not exist.
[0.8.0] - 2026-09-28
Added
- Agent setup prompt — a "Set up with an AI agent" dialog that mints a fresh
write-scoped API key (ingest + reads + non-admin mutations, never key-minting) and renders a copyable prompt handing a coding agent everything it needs: the deployment endpoint, project id, the key itself, plus tasks to instrument error/metric/log/heartbeat reporting per component (POST /api/v1/ingest— works from any language), register the app's databases viadb-connections(postgres → cassandra), wire the MCP server / CLI for future agents, and verify with a test event + deploy marker. Opens at the end of the new-project onboarding and is re-reachable from the project dashboard and Projects page; keys are shown only inside the prompt (revoke under Settings → API keys). - 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. (Hardened to a strict allow-list — see Security.) - 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. - S3-compatible object storage backend —
BACKEND_STORAGE_DRIVER=s3swaps the local disk store for any S3-compatible API (AWS, MinIO, Garage, R2) via a stdlib-only SigV4 implementation — no AWS SDK dependency.S3_ENDPOINT/S3_REGION/S3_BUCKET/credentials configure it;S3_PATH_STYLE(default on) covers MinIO/Garage, off for AWS virtual-hosted. The configured bucket is created on startup if missing; objects stream with computed SHA-256,storage_pathrecordss3://bucket/key. Verified live end-to-end (bucket → put → get → copy → list → delete). - Functions (
core.functions+core.function_runs, migration 00015) — project-scoped JS/TS executed by a runtime binary on the backend host (bunordeno, per function). The JSON payload arrives on stdin and asPRIMORA_PAYLOAD; stdout/stderr are captured per run (capped at 64 KiB) with status, exit code and duration —FUNCTIONS_RUNTIME/FUNCTIONS_TIMEOUT_SECONDS/FUNCTIONS_MAX_OUTPUT_BYTESconfigure the runner. API: CRUD under/projects/:id/functions,POST …/invoke(502 + recorded run on failure),GET …/runs. Dashboard gains a Functions page: code editor, JSON payload invoke, live result, run history. Code executes with backend privileges — the documented self-hosted trust model. - Presigned object URLs —
POST /buckets/:bucketID/object-presignsmints SigV4 query-signedGET/PUTURLs (60 s–1 h TTL) so clients transfer large objects directly against S3 without proxying through the backend. Read role for downloads (object must exist — no key probing), write role for uploads.S3_PUBLIC_ENDPOINTrewrites the client-facing host for internalS3_ENDPOINTs; signatures bind the host so URLs are valid only where they were minted. The local driver returns a clear error — a presigned URL needs a real object API. - Desktop auto-updater —
tauri-plugin-updaterwired end to end: minisign keypair,createUpdaterArtifacts,latest.jsonmanifest built from signed artifacts on every release, and a silent background check on the connect screen offering "Update & restart" (download → verify → install → relaunch). Unsigned artifacts can never install — missing signature means no manifest, not a bypass. CI signs viaTAURI_SIGNING_PRIVATE_KEYsecrets; OS-level signing (notarization/Authenticode) remains open. - Release SBOMs — CycloneDX SBOM for each published GHCR image (backend/auth/frontend) plus one for the source tree, attached to every GitHub Release via
anchore/sbom-action. - Sandboxed function driver —
FUNCTIONS_DRIVER=dockerruns every invoke in an ephemeral locked-down container: no network, read-only rootfs, 128 MiB memory, 0.5 CPU, 64 pids, non-root, all capabilities dropped. Source is injected via base64 env (no bind mount — works on Docker Desktop and remote daemons); timeout force-removes the container so nothing lingers. Verified live: code runs, network is unreachable inside, and deadline kills land. - Client events + presence —
POST /projects/:id/eventspublishes client-authored events (custom.*namespace enforced — system types can't be spoofed) through the same fan-out as system events: realtime subscribers, matching webhooks, andevent_patternfunctions all fire. Realtime now tracks presence:presence.update{online} broadcasts on every subscribe/unsubscribe (stream-only — never reaches webhooks or functions),GET …/realtime/presencereads the count. Client SDK gainspublish()andpresence(); CLI gainsevents:send. - Function triggers (migration 00016) — three new ways to run a function, all through the same executor and run log. Scheduled jobs accept
function_idinstead ofurl(run status mirrors the exit code); inbound hooks gainmode: "function"— the received body becomes the function's stdin payload, acked immediately with the run recorded async; andevent_patternon a function (document.*,*, or an exact type) fires it detached whenever a matching domain event publishes —{event, project_id, occurred_at, data}as payload. Runs record trigger source:manual/schedule/hook/event. Dashboard: function event-trigger field, job target picker (URL or function), and inbound-hook function mode. GET /api/v1/context— actor self-discovery: for API keys returns scopes, key prefix, and the resolved organization + project (id/slug/name). Fixes the API-key onboarding dead end:primora login --api-keynow validates the key against the instance before saving and seeds org/project context automatically;primora use,context, andwhoamiwork under API-key auth via this endpoint.- CLI
orgs:rm/projects:rm/buckets:rm— destructive deletes gated behind--yesin non-interactive mode, interactive confirm otherwise.buckets:rmresolves id/slug/name. - Webhook wildcard subscriptions —
eventsaccepts*,<namespace>.*, andcustom.*patterns, matched at dispatch time via SQLLIKE— the same semantics functionevent_patternalready used. Client-publishedcustom.*events can now be webhooked.
Security
primora_vault_execrewritten as an allow-list — the deny-list was bypassed by colon-form commands (["secrets:get","X"]as a single token evaded the pair check), exposing every vault secret;--remotevariants revealed project secrets; and thesecretparam was used verbatim as an env name, allowingNODE_OPTIONS=--require <file>host code execution. Now only a fixed read-mostly command set runs (whoami,context,orgs:list,projects:list,buckets:list,objects:list,documents:list,jobs:list,jobs:runs,audit:list,keys:list,events:send), path/file flags and..args are rejected, injected env names must match^[A-Z_][A-Z0-9_]{0,63}$, and loader-hook names (NODE_*,LD_*,DYLD_*,PATH,BASH*,PYTHON*,GIT_*,SSL_CERT*,XDG_*, …) are blocked. Verified: every prior bypass denied, allowed commands still inject.- SSRF hardening on webhook/job URLs — cloud metadata endpoints (
169.254.169.254,fd00:ec2::254) are excluded from the link-local/private allowance, and pure decimal/hex "bare hostnames" (2130706433,0x7f000001— non-canonical IP encodings of loopback) are no longer treated as Docker service names. - Path params now
encodeURIComponentin all generated-client consumers (CLI, MCP, dashboard) —encodeURIleft?#&intact, smuggling query segments through{name}params (e.g.secrets/a?reveal=1). - Strict base64 in
primora_upload_object—Buffer.from(b64)silently drops invalid characters;contentBase64is now validated (whitespace-tolerant) so corrupt input can't silently store garbage bytes. - MCP no-creds startup — a missing config/API key previously threw inside
configureClient, killing the process mid-handshake (protocol hang, stack on stderr). Startup now degrades:initialize/tools/listanswer normally and every tool call returns the actionable error.
Fixed
- Silent CLI dispatch — unknown commands, a bare
primora, and group names without a verb exited 0 with no output. Now: unknown input errors with exit 1 (bare groups list their verbs), bareprimoraprints help. - Arg-parser numeric coercion —
cac/mricoerced numeric-looking flag values (--value "007"→7,--value ""→0), corrupting secrets and keys. Parsed options are now re-read as raw strings from argv for every non-boolean flag;--value 007round-trips intact. pgx.ErrNoRows→ 404 in the shared error mapper — missing resources returned raw "no rows in result set" 500s across all handlers.- Structured error hygiene —
pgconn.PgErrorcodes map to clean statuses (23505 → 409 "resource already exists", 23503 → 409, 22001/54000 → 400 "value too large", 22021 → 400) instead of leaking SQLSTATE/constraint names; validator failures reportfield is required/field failed <tag> validation; a newservices.InputErrortype maps service-layer input failures to 400 (webhook URL/event-type errors were 500s). Presign on the local driver returnspresign_unsupported400. fetch failedpolish — connection failures report the cause code and suggestprimora contextinstead of bare undici errors.login --json— the flag now emits JSON on both the session and API-key paths.- Dashboard fonts vendored — Geist/Geist Mono now ship via
@fontsourceinside the bundle instead of a Google Fonts<link>the nginx CSP blocked (style-src 'self'). Removes the runtime Google dependency, works airgapped, and eliminates the console CSP violation. - Release image builds moved off QEMU — arm64
npm ciunderdocker/setup-qemu-actionSIGILLed on currentnode:20-alpinelayers (uncaught target signal 4) and wedged the build step for over an hour. Images now build per-arch on native runners (ubuntu-24.04-armfor arm64) and amanifestsjob joins them into the multi-arch:<version>/:latesttags.
[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.