Changelog

Release archive

Older Pushify releases. The latest updates live on the main changelog.

v0.2.0-beta.38

Dashboard

Added

  • Logs tab — full-page logs explorer (pairs with backend beta.44). Logs escape the modals: a dedicated project tab with two modes. Live — real-time container tail (SSE) with follow/pause auto-scroll, client-side filtering, reconnect, clear and download. History — server-side search over the last 7 days of persisted logs with stdout/stderr filter and chunk-level timestamps, downloadable. EN/TR i18n (logs namespace).

v0.2.0-beta.37

Dashboard

Added

  • Persistent Volumes section in project Settings (pairs with backend beta.43): attach named volumes at a container path so SQLite files, uploads and caches survive redeploys. Add/delete with client-side name/path validation; copy notes that changes apply on the next deploy and volumes are removed with the project. EN/TR i18n (volumes namespace).

v0.2.0-beta.36

Dashboard

Added

  • Cron Jobs tab. New per-project tab (pairs with backend beta.42): schedule commands that run inside the app container or periodic HTTP calls. Schedule presets (every 5/30 min, hourly, daily, weekly) plus raw 5-field cron input with timezone and timeout; per-task pause/resume, Run now, edit and delete; expandable run history showing status, trigger (manual/schedule), duration, exit code / HTTP status and captured output. EN/TR i18n (cron namespace).

v0.2.0-beta.44

Platform & API

Added

  • Project-level log search (GET /projects/:id/logs/search?q=&logType=&limit=) for the dashboard's new Logs explorer: searches the persisted 7-day container_logs chunks (ILIKE with escaped wildcards, literal substring semantics), extracts matching lines with their chunk timestamp / stdout-stderr type / deployment id. Bounded: max 100 chunks scanned, max 1000 lines returned.

v0.2.0-beta.43

Platform & API

Added

  • Persistent volumes for user apps. Projects can now attach Docker named volumes (pushify-vol-<slug>-<name>) at a chosen container path — SQLite files, uploads, caches survive every redeploy. Mounts are applied on all container start paths: standard remote deploy (blue-green), quick rollback, and local. New project_volumes table (migration 0032), REST CRUD (/projects/:id/volumes), validation (shell-safe names, absolute paths, /proc,/sys,/dev,/etc… mount targets denied), max 5 volumes/project. Changes take effect on the next deploy; project teardown removes the project's pushify-vol-* volumes.

Fixed

  • Blue-green traffic switch no longer drops container mounts. completeBlueGreenSwitch recreates the new container from docker inspect (image + env) but never carried over mounts — any volume (marketplace or user) would silently detach at the switch. The recreate now reads the container's volume/bind mounts and re-applies them.

v0.2.0-beta.42

Platform & API

Added

  • Cron jobs (scheduled tasks) for user apps. Per-project scheduled tasks with two types: command — a shell command executed *inside the app container* (docker exec over SSH on the project's server or runner, timeout-guarded, local fallback), and http — a GET to a URL (SSRF-guarded, timeout-bounded). Standard 5-field cron expressions with an IANA timezone per task (croner). New tables scheduled_tasks + scheduled_task_runs (migration 0031): precomputed next_run_at claimed atomically (CAS) by a 30s worker tick so each firing runs exactly once even with multiple workers; run history keeps exit code / HTTP status / captured output (8KB cap) for 7 days. REST: list/create/update/delete, Run now (POST .../run, synchronous, recorded as trigger=manual), and run history. Caps: 10 tasks per project, 10–600s timeout. Full migration chain re-verified on a fresh Postgres 16.

v0.2.0-beta.35

Dashboard

Added

  • Public `/changelog` page. Every release across the platform, API and dashboard on one timeline — parsed from both repos' CHANGELOG.md (local checkout first, GitHub raw as fallback once the repos are public), ISR-refreshed hourly so new releases appear without a redeploy. Footer link now points here instead of GitHub; added to the sitemap.
  • Discord notification channel UI. Fourth channel type in the project Notifications tab (webhook-URL field, embeds preview parity with backend beta.41); Discord icon wired into the alerts center and header alerts menu. EN/TR i18n.
  • Copy env vars between environments. New "Copy Between Environments" action in the Environment tab — pick source/target (production/staging/development/preview) and optionally overwrite; uses the existing /env/clone endpoint that previously had no UI.

Changed

  • Live metrics and health-check updates. The project page now subscribes to the metrics:update and healthcheck:result WebSocket events (hooks existed but were never mounted): charts and health logs refresh on push instead of waiting for the 15–30s polling intervals; time-series chart invalidation added for every hours-window.

v0.2.0-beta.34

Dashboard

Added

  • Docker image for self-hosting. New multi-stage Dockerfile producing a Next.js standalone build (enabled output: 'standalone' in next.config.ts — no effect on next dev). The backend's selfhost/docker-compose.yml + install.sh use it to run the dashboard as part of the one-command self-host stack. Note: NEXT_PUBLIC_API_URL is inlined at build time, so it's a build arg — changing the API URL requires rebuilding the image.

v0.2.0-beta.41

Platform & API

Added

  • Discord notification channel. New discord channel type alongside slack/email/webhook: paste a Discord incoming-webhook URL and deployment/health events arrive as rich embeds (event color, project/branch/commit/status fields, log tail on failures, link back to the dashboard). Implemented on both delivery paths (direct send in notification.service with the SSRF guard, and the BullMQ notification.worker). Migration 0030 adds the enum value. Note: the auto-generated migration was hand-trimmed to just the enum change — the schema had drifted from the migrations dir (earlier releases used db:push), and the generated file would have re-added already-existing columns and broken fresh installs; the new 0029_snapshot.json now captures the full current schema so future generates diff cleanly. Full migration chain verified against a fresh Postgres 16.

v0.2.0-beta.40

Platform & API

Added

  • Regression tests for this week's production incidents (25 new tests; suite now 45). Each locks in a bug that actually bit: redis-connection.test.ts — BullMQ must honor the DB index in REDIS_URL (staging/prod queue collision); runner-routing.test.ts — sticky runner-pool assignment, legacy env fallback, and resolveProjectServerId preferring the assigned server (the "Container is not running" log-stream bug); effective-plan-limits.test.ts — planLimitsOverride must reach effective limits without mutating shared plan definitions (the grant-org / disabled Create Server bug); utils.test.ts — normalizeClientIp first-IP + varchar(45) cap (the login 500 from long x-forwarded-for chains).

v0.2.0-beta.39

Platform & API

Added

  • One-command self-hosting. curl -fsSL .../selfhost/install.sh | bash now stands up the entire platform on any Docker host: it clones backend + frontend, generates secrets (JWT_SECRET, ENCRYPTION_KEY, DB password), and starts a full stack — dashboard, API, worker, one-shot migrator, Postgres 16, Redis 7 — via selfhost/docker-compose.yml. New multi-stage Dockerfile builds a single backend image used for the API (dist/index.js), the worker (dist/worker.js) and migrations. New src/migrate.ts (built to dist/migrate.js, npm run db:migrate:prod) applies Drizzle SQL migrations programmatically so production doesn't need drizzle-kit/dev deps. Full guide in docs/SELF_HOSTING.md — including the deploy model: the containerized control plane has no host Docker/nginx access by design; apps deploy to servers attached over SSH (which can be the same machine), exactly like Pushify Cloud.
  • README: Self-Hosting section; fixed the frontend repo link (pushify-dev/pushify-frontend → pushifydev/pushify_frontend).

v0.2.0-beta.38

Platform & API

Fixed

  • Container logs now work for runner-deployed projects ("Container is not running"). The log-stream endpoint decided remote-vs-local solely from project.serverId, so a free/unassigned project — which has no serverId but still deploys to a runner via sticky routing — was treated as local, looked at the control-plane's Docker, found nothing, and returned *"Container is not running."* Extracted the deploy-target resolution into lib/runner-routing.ts (pickRunnerServerId + resolveProjectServerId = project.serverId || runner) — the same logic the deploy worker uses — and the log stream now SSHes into the actual runner. (Other serverId-gated container ops, e.g. custom-domain nginx in domain.service, should adopt resolveProjectServerId too as runner usage grows.)

v0.2.0-beta.37

Platform & API

Fixed

  • BullMQ now honors the Redis DB index in `REDIS_URL` (e.g. redis://host:6379/1 → db 1). The queue connection previously parsed only host/port/auth and silently dropped the db, so every environment sharing a Redis host landed on db 0 with the same static queue names (deployments, notifications, …). On a box running both staging and production, that meant a staging worker could pick up and run a production deployment job (and vice-versa) — which also silently broke runner routing: the staging process lacks PUSHIFY_RUNNER_SERVER_IDS, so it fell back to a local deploy and failed with *"Docker is not available on this machine."* Centralized the connection parsing (lib/redis-connection.ts) across the enqueue side and all queue workers (deployments, notifications). To isolate environments sharing one Redis, point each at a different db — e.g. production …/0, staging …/1.

v0.2.0-beta.36

Platform & API

Fixed

  • Dashboard no longer rate-limits itself (429 storm). The interactive dashboard polls logs, status and metrics, so an active session can easily exceed the free plan's 60 req/min — but that per-plan apiRequestsPerMinute was meant for *programmatic* API keys, not the browser. Authenticated session (JWT) traffic now uses a separate, generous per-org limit (RATE_LIMIT_SESSION_MAX, default 600/min) that only guards against a runaway client loop; API keys keep their per-plan limit unchanged. This removes the constant 429s while managing a project (especially during a deploy) without weakening API-key tiering or anonymous/IP flood protection.

Note

  • Behind a reverse proxy (nginx), set `TRUSTED_PROXY_HOPS=1` in the control-plane .env so IP-based limiters (login/refresh) use the real client IP instead of bucketing every user under the proxy's address.

v0.2.0-beta.33

Dashboard

Added

  • Multi-line environment variable values. The add-variable and edit-value fields are now resizable textareas, so you can paste a value that spans multiple lines — e.g. a Firebase / service-account PEM private key — and it's stored and injected with real newlines intact (the whole pipeline already preserved them; only the single-line input was the limit). No code-side \n juggling needed. In the inline editor, plain Enter inserts a newline and Cmd/Ctrl+Enter saves (Esc cancels). The .env bulk-paste parser is unchanged.

v0.2.0-beta.32

Dashboard

Added

  • Edit environment variable values inline. The Environment tab only let you reveal/delete a variable, so changing a value meant deleting and re-adding it. Each variable now has an Edit (pencil) action that turns the value into an inline input with Save/Cancel (Enter saves, Esc cancels). Wired to the existing useUpdateEnvVar hook / backend PATCH /:envVarId — the value is updated and applied on the next deploy. (Renaming a key is still delete + add.)

v0.2.0-beta.35

Platform & API

Added

  • Internal/comp grant tool (npm run grant-org): set an organization's plan and/or planLimitsOverride directly, without Stripe — for the operator's own org (e.g. --servers 1 to add the platform runner) or to comp a partner. Uses the existing per-org planLimitsOverride (merged on top of plan limits by getEffectivePlanLimits), so the org keeps its real billing state but gains the granted capacity. Examples: grant-org -- --org <id> --servers 1, --plan hobby, --override '{"servers":2,"projects":20}'.

v0.2.0-beta.34

Platform & API

Changed

  • Runner is now a pool, not a single server. PUSHIFY_RUNNER_SERVER_IDS (comma-separated servers row ids) defines a pool of dedicated runner hosts that free/unassigned deploys land on; a project is stickily and deterministically mapped to one runner by its id, so its redeploys always go to the same host (its subdomain/state stay put) while projects spread across the pool. Scales to N runners by just adding ids — no code change. The single PUSHIFY_RUNNER_SERVER_ID from beta.33 is still honored as a one-runner pool. (Note: actually serving 2+ runners also needs per-app DNS so <app>.pushify.dev resolves to that app's runner — a wildcard A record only points at one host; that piece comes when the 2nd runner is added.)

v0.2.0-beta.33

Platform & API

Added

  • Free/unassigned deploys can target a dedicated runner server instead of running on the control-plane host. New PUSHIFY_RUNNER_SERVER_ID env (a servers row id): when a project has no user-assigned serverId, the deploy now lands on that runner over SSH (same remote path as a user's own server) — keeping untrusted free-tier workloads off the box that runs pushify.dev + the API + the platform database. If unset, behavior is unchanged (deploys fall back to the local host). The deploy-target resolution (deployTargetServerId = project.serverId || PUSHIFY_RUNNER_SERVER_ID) is applied across the standard, static-site, blue-green and quick-rollback paths; per-project server assignment, cleanup and GC still key off the user's real serverId.

v0.2.0-beta.32

Platform & API

Changed

  • Hardened the domain → port resolution (from beta.31) so it can't drift on any deploy path. resolveProjectPort now uses the actual host port recorded on the most recent deployment as the source of truth (every deploy path — blue-green, marketplace, quick-rollback — records the real port it published on), falling back to the PORT env and then the assigned port only if no deployment port is available. This also covers the edge case of a marketplace app with a manually-set PORT plus a custom domain.

v0.2.0-beta.31

Platform & API

Fixed

  • Custom domains no longer 502 when the app uses a custom `PORT`. A deploy publishes the app on the project's PORT env value when set (e.g. 1367), but the domain → Nginx setup derived the proxy target from the *assigned* port (getOrAssignPort) instead, so the vhost proxied to the wrong port and returned 502 even though the app was reachable on its real port. Domain verification and the apply-settings path now resolve the app's actual published port the same way the deploy does (PORT env first, assigned port as fallback) via a shared resolveProjectPort helper.

v0.2.0-beta.30

Platform & API

Fixed

  • Apps can now reach a Pushify-managed database by its container name. A standalone database (pushify-db-<name>) runs on the default bridge network, where Docker provides no name resolution — so a deployed app connecting to pushify-db-<name> failed with ENOTFOUND, and there was no reliable host to use instead. Every remote deploy (standard, blue-green, and quick-rollback) now joins the app and all pushify-db-* containers to a shared pushify Docker network, so the app reaches its database by container name with no host/IP guessing. Scoped to servers that actually have Pushify databases (i.e. the user's own server) — the shared host is unaffected. The database's public-port choice is untouched (still the user's decision); this only adds private app↔database connectivity.

v0.2.0-beta.29

Platform & API

Added

  • Automatic GC of orphaned deployments on the shared host. When a project is moved to a user's own server (or deleted), its container/images can linger on the shared Pushify host and waste disk. A new periodic sweep (gcOrphanedLocalDeployments, every 30 min from the background workers) tears down any local pushify-<slug> deployment whose project no longer belongs there — safe by construction: it only acts on projects that have a remote serverId (so they must not have a local deployment) or are deleted, matched by exact name. Also runnable on demand with npm run gc:orphans.

Changed

  • The teardown script (buildRemoteTeardownScript, used by project delete, server-move cleanup, and the new GC) now also removes the project's built images (pushify/<slug>*), not just the containers/vhost/dir — so cleanups actually reclaim the disk that images consume.

v0.2.0-beta.28

Platform & API

Fixed

  • **Projects on a user's own server no longer try to use a *.pushify.dev subdomain.** A *.pushify.dev auto-subdomain only works on Pushify's shared host (where the wildcard cert lives and the *.pushify.dev DNS points). When a project that was created on the shared host got moved to the user's own server, the deploy still tried to configure that same subdomain on the user's server — which failed (nginx -t: wildcard cert not present) and was conceptually wrong (the subdomain resolves to Pushify's host, not the user's server). Both remote-deploy paths (standard + blue-green) now check whether the target server actually has the wildcard cert; if a carried-over auto-subdomain isn't usable there, the auto-generated domain record is removed and the project is served over the server's IP (http://<ip>:<port>) instead. A user's own server only gets a public domain when they add their own (which then gets a real per-domain Let's Encrypt cert).

v0.2.0-beta.27

Platform & API

Fixed

  • Moving a project to a different server now tears down the old host. Changing a project's deployment server (local→server, server→server, or server→local) only updated serverId — the old container/images kept running on the previous host (e.g. the shared Pushify server), wasting resources and potentially still answering on the old subdomain. updateProject now detects a server change and runs the existing best-effort cleanup (cleanupProjectContainers) against the previous host (using the pre-update snapshot) so the old deployment is removed; the next deploy lands on the new server. (Reminder: assigning a server still requires it to be running + setup completed, otherwise the assignment is rejected and the project stays where it is.)

v0.2.0-beta.26

Platform & API

Fixed

  • Auto-subdomain Nginx config now points to the actual wildcard cert. The *.pushify.dev vhost hardcoded its certificate path from PREVIEW_BASE_URL (/etc/letsencrypt/live/<base>/), ignoring WILDCARD_SSL_PATH. When the wildcard cert lives under a different lineage name (e.g. the apex pushify.dev cert already owns /etc/letsencrypt/live/pushify.dev, so the wildcard is at .../pushify.dev-0001), Nginx looked for a cert that wasn't there → cannot load certificate ... No such file → nginx -t failed → the config was never applied. generateAutoSubdomainSiteConfig now uses WILDCARD_SSL_PATH when set (falling back to the base-domain path), matching the deploy workers' existing resolution. Set WILDCARD_SSL_PATH to the wildcard's live dir.

v0.2.0-beta.25

Platform & API

Fixed

  • Login no longer 500s behind a proxy chain (value too long for type character varying(45)). The client IP was taken straight from the x-forwarded-for header and written to the ip_address varchar(45) column — but behind multiple proxies (nginx/Cloudflare) that header is a comma-separated list of IPs that overflows 45 chars, which threw on session creation and broke every login (password + Google/GitHub OAuth). Added a shared normalizeClientIp() that keeps only the first (client) IP and caps it to 45 chars, applied at session creation and activity logging. Not data/Postgres related — purely the header length.

v0.2.0-beta.24

Platform & API

Fixed

  • Servers no longer get stuck at setup `installing` after they're running. The reconciliation sweep now also covers the *setup* stage, not just provisioning: for managed servers that reached running but whose setupStatus is still pending/installing (e.g. the one-shot server-setup poll job died on a worker restart, even though the server's /health is up), the sweep re-checks http://<ip>/health every 30s and marks the server completed (sending the server-ready email) once it responds — or failed only after a generous 30-minute deadline. This closes the same silent-stuck gap on the setup stage that beta.23 closed on provisioning. npm run requeue:stuck-servers (which runs the sweep) recovers any already-stuck servers immediately.

v0.2.0-beta.23

Platform & API

Fixed

  • Managed servers no longer get stuck at "Provisioning" forever. The one-shot server-status poll job only retries for ~5 minutes; if the VM reaches running on the provider later than that (slow boot, or the worker process restarted mid-poll), the job exhausted and gave up silently — leaving the server stuck at provisioning even though the VM was actually running. Added an independent provisioning reconciliation sweep (reconcileProvisioningServers, runs every 30s from the background workers) that re-checks every server still stuck at provisioning and drives it forward: promotes it to running and hands off to setup as soon as the provider reports it ready (idempotent — the fixed setup jobId de-dupes), or marks it error with a clear retryable message if the provider errored or it's been provisioning past a 20-minute deadline (no more silent dead-ends). The existing poll/setup workers are unchanged — this is a safety net.
  • Added scripts/requeue-stuck-servers.ts (npm run requeue:stuck-servers) to immediately recover any servers already stuck at provisioning (or running with setup still pending).

Fixed

  • Storage quota no longer unfairly blocks deploys ("Monthly storage limit reached"). Three compounding bugs are fixed: - Enforcement rounded any usage up to a full GB. The check compared ceil(bytes / GB) >= limit, so a free org (1 GB) was blocked the moment *any* storage was recorded. It now compares real bytes against limit × GB. - Per-deploy cumulative inflation. Each deploy added its image size to a running "peak" that never decreased (even though only the last 5 images are kept on disk), so redeploys eventually tripped the limit. Storage is no longer metered by cumulative deploy size. - Whole-host over-counting on shared hosts. Storage was measured as the entire host's docker disk and attributed to *every* org on it. It's now measured per-org as the footprint of that org's own project images (pushify/<slug>), so co-tenants aren't charged for each other's images or for build cache.
  • Free-tier storage allowance raised from 1 GB to 5 GB.
  • Added scripts/reset-storage-usage.ts (npm run reset:storage-usage [-- --org <id>]) to clear the previously-inflated current-period peaks; the next disk sync re-records the correct per-org footprint.

v0.2.0-beta.31

Dashboard

Security

  • The Google and GitHub OAuth callbacks now honor two-factor authentication. When the backend returns a requiresTwoFactor challenge (for an account with 2FA enabled), the callback stores the challenge token and hands off to the existing 2FA form on /login instead of trying to read tokens that aren't there — so OAuth sign-in goes through the same second-factor step as password login. (Pairs with backend 0.2.0-beta.21.)