SprintFlint now runs on Railway. The Rails web app, background worker, public MCP service, PostgreSQL, Redis, private uploads and daily backups live in one Railway project, with production resources in Amsterdam.
We completed the application cutover on 1 October and the remaining storage, deployment and retirement checks on 2 October. Teams keep using sprintflint.com and the same accounts, tickets and integrations. The public MCP endpoint remains at mcp.sprintflint.com.
The goal was a clearer place to operate the application: its services, data, file storage, release configuration and recovery process together. Here is what moved, what we checked, and what that means for the product.
One project, separate jobs
SprintFlint already ran its web application and MCP server from the same Rails codebase. We kept that shape and gave each responsibility its own Railway service:
| Component | Role on Railway |
|---|---|
| Web | Serves the application at sprintflint.com |
| Worker | Runs Sidekiq background jobs |
| MCP | Serves the authenticated public MCP endpoint |
| PostgreSQL | Holds accounts, organisations, projects, sprints and tickets |
| Redis | Supports background jobs and real-time connections |
| Object Storage | Holds private uploads and a separate backup bucket |
| Backups | Runs the daily backup process |
Keeping the worker and MCP service separate means each has an explicit start command and a release we can check. They share the application’s data and configuration. Database migrations run once, from the web service’s pre-deploy step, so the worker and MCP service do not attempt the same migration concurrently.
Cloudflare continues to handle domain registration, DNS and the existing email-related records. We updated the application hostnames to point at Railway and checked their TLS certificates and the www redirect. The existing email-delivery and error-reporting integrations, including Postmark and Sentry, remain part of SprintFlint.
A successful import needed more than a running server
The PostgreSQL move started with a final Heroku backup. After restoring it, we compared every application table’s row count and row fingerprints against the source. A fingerprint check helped confirm the contents matched as well as the totals.
Redis needed a different approach. The source’s serialized DUMP format could not be restored into our target Redis version. We transferred the logical values instead, then checked key types, values, list order, sorted-set scores and absolute expiry times. That distinction mattered: a Redis connection can be healthy while the queue or cached state it was supposed to preserve is incomplete.
We also kept the existing application signing secret and encrypted-credentials key. During verification, an authenticated browser session remained signed in after a Railway redeploy, and a real Google login reached the existing account. The original API and MCP authentication tokens continued to work.
For teams, the useful result is continuity: their existing workspace and ticket history came across with the infrastructure.
Uploads moved from R2 to private Railway storage
The database is only part of a ticketing application’s state. Screenshots and attachments need to survive the move too.
We copied the upload objects from Cloudflare R2 into private Railway Object Storage and checked the SHA-256 checksum of every copied file. Development, staging and test received their own separate storage configuration as well. Rails tests continue to use in-memory upload storage.
The application keeps its existing upload integration, with the storage endpoint and credentials changed for Railway. We verified the browser flow through a signed upload URL, its CORS preflight, and reading the uploaded file back. Runtime image processing and MIME detection were checked with the required dependencies installed.
Before retiring R2, we also checked stored database values for references to the old Cloudflare storage URLs. The source objects and SprintFlint R2 buckets were removed after the transfers and recovery copies had been verified.
Daily backups, with a completion check
The scheduled backup service runs at 03:00 UTC each day. It writes three kinds of archive to a separate private Railway bucket:
- A PostgreSQL custom-format dump.
- Redis key payloads with their expiry times.
- A ZIP of uploaded files, including original object keys, content types, metadata and checksums.
Each archive is read back from storage and checked against its SHA-256 checksum. Only after every archive passes does the job write a completion manifest. That gives us a concrete way to distinguish a finished snapshot from a partially uploaded attempt.
Daily snapshots have 30-day retention. Expiry cleanup happens after a successful new backup, and a failed backup exits with an error visible in the service logs. The final migration recovery snapshot is kept separately from that daily expiry policy.
We verified successful manual and scheduled runs. We also restored a PostgreSQL backup into a temporary verification database and checked its tables. Reading back an archive and testing a database restore answer different questions; both were part of this move.
We checked the release path as well
The web, worker, MCP and backup services now follow the repository’s master branch and wait for GitHub checks before automatically deploying.
There was a useful setup lesson here. Connecting a repository source was enough to build a release, but our automatic deployment triggers did not become usable until the deploying Railway account was linked to GitHub as well. We checked the actual triggers and CI settings, then merged through a connected account and observed all four services advance from waiting for checks to successful releases.
We kept the project’s release approval safeguards. Releases from an automation account that is not linked to the project require review and approval. Passing GitHub checks and seeing the resulting Railway deployment succeed are separate checks in our release process.
The live checks that closed the migration
Before calling the move complete, we verified the paths people and integrations actually use:
- The public site, authenticated dashboard and Google sign-in.
- Database and Redis health, plus an actual harmless Sidekiq job completing.
- A live WebSocket connection and confirmed subscription.
- Authenticated MCP initialisation, tool discovery and a read-only project request.
- Signed uploads, browser CORS and file readback.
- The live application’s release identifier matching the deployed GitHub revision.
- Backup archive checksums and the temporary PostgreSQL restore.
After those checks, we retired the old Heroku applications and their database and Redis add-ons. SprintFlint’s old R2 buckets were retired too. Cloudflare’s domain, DNS and email settings remain in place.
The product still gives teams the same place to plan sprints, manage tickets and follow progress. The change underneath is a more coherent operating setup: named services in one project, a verified route from a reviewed commit to a running release, checksum-verified backup archives and a tested PostgreSQL restore.