Project setup
Scaffold a Railbase app, understand the layout, and run the dev loop.
Updated
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.