# Project setup

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

_Updated: 2026-06-10_

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

```bash
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:
>
> ```text
> replace github.com/railbase/vault => /path/to/vault
> ```

`railbase init` generates a complete, buildable project:

```text
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:

```go
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

```bash
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](installation#development-vs-production).) 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](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:

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

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