Railbase
GPTClaude

Project setup

Scaffold a Railbase app, understand the layout, and run the dev loop.

Updated

Video guide —watch on YouTube ↗

Railbase isn't only something you run — it's something you build on. You define your data model in Go, and Railbase gives you a REST + realtime API, auth, an admin console, and a typed client for free. This section is the developer track.

Scaffold a project

railbase init myapp --template basic
cd myapp
go mod tidy

Run railbase init with no name or --template at a terminal and it prompts interactively (the npm create vite model); pass both to skip the prompts in CI.

Important

Public binary releases scaffold projects without requiring Railbase source. Private partner/source checkouts may use --railbase-source or RAILBASE_LOCAL_PATH to point the scaffold at a local core checkout; that is a development workflow, not something an operator needs to run Railbase.

railbase init generates a complete, buildable project:

myapp/
├── cmd/myapp/main.go     # entry point (your binary)
├── schema/main.go        # your data model (Go DSL)
├── pb_hooks/example.pb.js# server-side JS hooks
├── railbase.yaml         # project config
├── webembed/             # optional embedded SPA
├── Makefile
├── go.mod
└── pb_data/              # vault + .secret + .vault_password (created on init)

The templates layer on each other: basic is backend-only; auth-starter adds a Preact + Vite + Tailwind frontend with the account UI (sign-in, profile, password, sessions, 2FA); fullstack adds the full public/private app shell on top of that — landing/pricing/contact pages, a dashboard, and tenants + members + RBAC screens.

Tip

The project directory and Go module both default to the <name> you pass. Override the module path with --module github.com/you/myapp while keeping the directory name.

The entry point

cmd/myapp/main.go is a thin main that hands control to Railbase and gives you a hook to extend the app before the server starts:

package main

import (
    _ "myapp/schema" // blank import: its init() registers your collections

    "github.com/go-chi/chi/v5"
    "github.com/railbase/railbase/pkg/railbase"
    "github.com/railbase/railbase/pkg/railbase/cli"
    "github.com/railbase/railbase/pkg/railbase/hooks"
)

func main() {
    cli.ExecuteWith(func(app *railbase.App) {
        // Runs after the app is built, before Run() starts. Safe here:
        // Go hooks (the registry is lazy).
        app.GoHooks().OnRecordBeforeCreate("posts",
            func(c *hooks.Context, ev *hooks.RecordEvent) error {
                // ... validate, mutate ev.Record, etc.
                return nil // or an error / hooks.ErrReject → 400
            })

        // Jobs / JobsStore / Realtime / EventBus are wired during
        // App.Run and are nil at this point — register against them
        // from OnBeforeServe, which fires after every subsystem is up:
        app.OnBeforeServe(func(r chi.Router) {
            app.Jobs().Register("report.generate", generateReport)
            app.EventBus().Subscribe("record.changed", 256, onRecordChanged)
            r.Get("/api/myapp/stats", statsHandler(app))
        })
    })
}

Useful app seams: OnBeforeServe(func(chi.Router)), GoHooks(), ServeStaticFS(path, fs), Jobs(), JobsStore(), Realtime(), EventBus(), Pool(). Everything except GoHooks/OnBeforeServe/ ServeStaticFS returns nil before Run — touch those only from inside an OnBeforeServe callback (or later, e.g. in hooks and handlers).

First run

go build ./cmd/myapp
./myapp migrate diff initial_schema   # generate the first migration from your DSL
./myapp migrate up                    # apply it
./myapp serve                         # http://localhost:8095  (admin at /_/)

The scaffolded project unlocks the vault with the development key out of the box — no RAILBASE_VAULT_PASSWORD needed. Two things put it there: railbase.yaml ships with runtime.dev: true, and init writes pb_data/.vault_password containing the well-known dev key, which serve/migrate auto-discover on every boot.

Caution

Before deploying, secure both: remove runtime.dev: true and delete (or replace) pb_data/.vault_password — otherwise the dev key still unlocks the vault even in production mode. Then supply a real password via RAILBASE_VAULT_PASSWORD_FILE. See Installation.

Seed the first admin with ./myapp admin bootstrap you@example.com and sign in (this first-run/recovery command creates the first administrator and refuses once one exists; --password / --no-email carry over). Add further admins routinely via the invite flow — admin UI → Users & access → Admins & roles → Invite, or POST /api/_admin/admins/invite. See Defining your schema next.

The dev loop

For active development, railbase dev runs the backend and a frontend dev server under one Ctrl-C, waits for /readyz, and (optionally) regenerates the TypeScript SDK on schema changes:

railbase dev --web ./web --web-cmd "npm run dev" \
             --watch-schema ./schema --sdk-pkg ./schema --sdk-out web/src/client

Logs are interleaved and prefixed [api] / [web]. --watch-schema only regenerates the SDK when --sdk-pkg (the schema package to re-run go run against) is also set; without it dev prints "SDK regen disabled" and just runs the two servers.

Was this page helpful?Thanks for your feedback!