Getting started
Cairn is a library, not a separate process. You construct a server, point it at a database, and serve — auth, sessions, and the rest come along. This page builds a new app from nothing to a working magic-link sign-in.
Prerequisites
Section titled “Prerequisites”- Go 1.25+
- Docker (optional — only for the local email inbox; a no-Docker fallback is shown below)
Install
Section titled “Install”mkdir myapp && cd myappgo mod init example.com/myappgo get github.com/benorfaz/oreb-cairn/cairnThe app
Section titled “The app”Create main.go:
package main
import ( "context" "log"
"github.com/benorfaz/oreb-cairn/cairn")
func main() { srv, err := cairn.NewServer(cairn.Config{ ServiceName: "myapp", Database: cairn.DB{Driver: "sqlite", DSN: "myapp.db"}, Auth: cairn.Auth{ BaseURL: "http://localhost:8080", SuccessURL: "http://localhost:8080/", MagicLink: cairn.MagicLink{ From: "no-reply@myapp.local", Mailer: cairn.SMTPMailer(cairn.SMTPConfig{Host: "localhost", Port: 1025, TLS: cairn.SMTPNone}), }, CookieInsecure: true, // local HTTP dev only }, }) if err != nil { log.Fatal(err) } if err := srv.EnsureSchema(context.Background()); err != nil { log.Fatal(err) } log.Println("listening on :8080 — sign in via http://localhost:8025") log.Fatal(srv.ListenAndServe())}That is the whole app: a SQLite database in the working directory, magic-link
sign-in delivering to a local inbox, and every Cairn RPC served on :8080.
EnsureSchema creates and migrates Cairn’s tables — it is idempotent, so
calling it at every boot is the intended pattern.
No Docker? Swap the Mailer line for cairn.LogMailer(nil) and the sign-in
link lands in your app’s logs instead of an inbox.
Run it
Section titled “Run it”Start Mailpit (the local inbox), then the app:
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpitgo run .Sign in
Section titled “Sign in”Request a magic link:
curl -s -X POST http://localhost:8080/cairn.v1.AuthService/RequestMagicLink \ -H 'Content-Type: application/json' -d '{"email":"dev@myapp.local"}'{}The response is always empty — Cairn never reveals whether an email is known
(see Hardening). The email itself is in Mailpit: open
http://localhost:8025, and the message “Your sign-in link” contains a URL of
the form http://localhost:8080/auth/magiclink?token=…. Follow it with a
cookie jar:
curl -s -i -c cookies.txt "http://localhost:8080/auth/magiclink?token=…"HTTP/1.1 302 FoundLocation: http://localhost:8080/Set-Cookie: cairn_session=…; Path=/; Max-Age=604800; HttpOnly; SameSite=LaxOne redirect to your SuccessURL, one hardened session cookie. (In
production the cookie also carries Secure — it is only absent here because
of CookieInsecure.) You are signed in:
curl -s -b cookies.txt -X POST http://localhost:8080/cairn.v1.SessionService/WhoAmI \ -H 'Content-Type: application/json' -d '{}'{"authenticated": true, "userId": "usr_01KXP444H30GFNY87MFABCZ1WX"}The user was provisioned on first sign-in — sign-up and sign-in are unified.
Mount your own services
Section titled “Mount your own services”Your app’s own RPCs are Connect services registered through the same interceptor chain Cairn uses, so they get authentication, rate limiting, and the caller’s identity for free:
path, handler := appv1connect.NewProjectServiceHandler(impl, srv.HandlerOptions()...)srv.Mount(path, handler)Inside a handler, cairn.ActiveWorkspace(ctx) yields the caller’s workspace —
see Extending the model.
Postgres
Section titled “Postgres”SQLite and Postgres are interchangeable; production runs Postgres. Only the
Database block changes:
Database: cairn.DB{Driver: "postgres", DSN: "postgres://…"},Next steps
Section titled “Next steps”- Data model — the entities Cairn just created in
myapp.db. - Sign-in — OAuth providers, passkeys, and mailers beyond the magic link.
- Build, test, deploy — from this walkthrough to production.