Skip to content

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.

  • Go 1.25+
  • Docker (optional — only for the local email inbox; a no-Docker fallback is shown below)
Terminal window
mkdir myapp && cd myapp
go mod init example.com/myapp
go get github.com/benorfaz/oreb-cairn/cairn

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.

Start Mailpit (the local inbox), then the app:

Terminal window
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit
go run .

Request a magic link:

Terminal window
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:

Terminal window
curl -s -i -c cookies.txt "http://localhost:8080/auth/magiclink?token=…"
HTTP/1.1 302 Found
Location: http://localhost:8080/
Set-Cookie: cairn_session=…; Path=/; Max-Age=604800; HttpOnly; SameSite=Lax

One 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:

Terminal window
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.

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.

SQLite and Postgres are interchangeable; production runs Postgres. Only the Database block changes:

Database: cairn.DB{Driver: "postgres", DSN: "postgres://…"},
  • 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.