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 \
  --railbase-source /path/to/railbase    # pre-release: pin the local core checkout
cd myapp
go mod tidy

Important

Pre-release: github.com/railbase/railbase isn't published yet, so --railbase-source (or RAILBASE_LOCAL_PATH) is required. It writes both replace directives into the generated go.mod — one for the core and one for the (also-unpublished) github.com/railbase/vault it depends on, assuming a vault/ checkout next to the railbase one. replace directives are not inherited from dependencies, so if your vault checkout lives elsewhere and go mod tidy fails with "github.com/railbase/vault@v0.0.0 … 404", point the second replace at the right path:

replace github.com/railbase/vault => /path/to/vault

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 (created on init)

The templates layer on each other: auth-starter = basic + auth, fullstack = auth-starter + a web frontend.

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 railbase.yaml ships with runtime.dev: true, so these commands unlock the vault with the development key out of the box — no RAILBASE_VAULT_PASSWORD needed. (Remove that line and set a real password before deploying; see Installation.) If you ever clear it and forget, migrate/serve exit with "no vault password configured" — re-add it or prefix the command with RAILBASE_DEV=true.

Create an admin with ./myapp admin create you@example.com and sign in. 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-out web/src/client

Logs are interleaved and prefixed [api] / [web].