Skip to content
Analytik
Esc
↑↓navigate↵open⌘Jpreview
On this page

Railway and Postgres

Run Analytik as a long-lived Bun service on Railway with a Railway Postgres database, a pre-deploy migration and a custom domain.

Railway runs Analytik as a normal long-lived process, which is the simplest model: one connection pool, no cold starts. The database is a Railway Postgres service in the same project.

Project

mkdir analytik-server && cd analytik-server
bun init -y
bun add analytik hono postgres
mkdir -p public/js && cp node_modules/analytik/dist/analytik.js public/js/analytik.js
import { createAnalytik } from 'analytik/server'
import { postgresStorage } from 'analytik/storage/postgres'
import { serveStatic } from 'hono/bun'
import { Hono } from 'hono'
import postgres from 'postgres'

const analytik = createAnalytik({
  storage: postgresStorage(postgres(process.env.DATABASE_URL!)),
  admin: { password: process.env.ADMIN_PASSWORD!, secret: process.env.SESSION_SECRET! },
  // Railway's proxy passes the visitor's address in X-Forwarded-For.
  trustProxy: 'forwarded',
})

const app = new Hono()
app.route('/', analytik.app)
app.use('/js/analytik.js', serveStatic({ path: './public/js/analytik.js' }))

export default {
  port: Number(process.env.PORT ?? 3000),
  fetch: app.fetch,
}

Railway sets PORT. Bind to it and nothing else.

Services

  1. Create a project and add a PostgreSQL database.
  2. Add a service from your repository (or railway up).
  3. In the service’s variables, add DATABASE_URL as a reference to the database’s DATABASE_URL so Railway keeps it in sync, and set ADMIN_PASSWORD (not empty) and SESSION_SECRET (at least 32 random characters, for example from openssl rand -hex 32). The server throws at startup if either is missing or too short.
  4. Set the start command to bun server.ts.
  5. Set a pre-deploy command so the schema is current before the new version takes traffic:
bunx analytik migrate --db "$DATABASE_URL"

Use the database’s private networking address (*.railway.internal) for DATABASE_URL. It avoids egress charges and keeps the database off the public internet. The pre-deploy command runs inside Railway’s network, so the private address works there too.

Domain

In the service’s settings, generate a Railway domain or add a custom one and create the CNAME record Railway shows. Use the resulting hostname in the script tag:

<script defer src="https://analytics.example.com/js/analytik.js" data-site="SITE_ID"></script>

Railway puts a proxy in front of the service and passes the visitor’s address in X-Forwarded-For. Analytik ignores that header unless you set trustProxy: 'forwarded', as in the code above, because otherwise a visitor could send it themselves. That mode reads the rightmost public address, so it is only safe behind a proxy that sets or appends the header. Check that this holds for your Railway setup. Without a client IP, strict mode tells visitors apart by user agent only. Railway does not add location headers, so countries and cities are empty unless you supply your own lookup with the geo option or put Cloudflare in front of the domain.

Costs and limits

Railway bills for resources used, and a small analytics server with a small database is usually a few dollars a month. The database grows with traffic, so set a retention period. Check Railway’s current pricing before you rely on any figure.

Was this page helpful?