diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..59bda18
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+# Binary
+cmd/leaf/leaf
+
+# Data directory
+data/
+
+# Go
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+*.test
+*.out
+
+# Frontend (SvelteKit build output managed separately)
+frontend/node_modules/
+frontend/.svelte-kit/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d7dfb10
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+.PHONY: build run test frontend all clean
+
+# Default data directory
+DATA_DIR ?= ./data
+ADDR ?= :8080
+BINARY := cmd/leaf/leaf
+
+build:
+ CGO_ENABLED=1 go build -o $(BINARY) ./cmd/leaf/
+
+run: build
+ ./$(BINARY) --data-dir $(DATA_DIR) --addr $(ADDR)
+
+run-dev: build
+ ./$(BINARY) --data-dir $(DATA_DIR) --addr $(ADDR) --dev
+
+test:
+ CGO_ENABLED=1 go test ./...
+
+frontend:
+ @mkdir -p frontend/build
+ @if [ ! -f frontend/build/index.html ]; then \
+ echo '
FeltFelt
Loading...
' > frontend/build/index.html; \
+ fi
+ @echo "Frontend build complete (stub)"
+
+all: frontend build
+
+clean:
+ rm -f $(BINARY)
+ rm -rf data/
diff --git a/cmd/leaf/main.go b/cmd/leaf/main.go
new file mode 100644
index 0000000..e95d469
--- /dev/null
+++ b/cmd/leaf/main.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/felt-app/felt/internal/store"
+)
+
+func main() {
+ dataDir := flag.String("data-dir", "./data", "Data directory for database and files")
+ addr := flag.String("addr", ":8080", "HTTP listen address")
+ devMode := flag.Bool("dev", false, "Enable development mode (applies dev seed data)")
+ flag.Parse()
+
+ log.SetFlags(log.LstdFlags | log.Lmsgprefix)
+ log.SetPrefix("felt: ")
+
+ log.Printf("starting (data-dir=%s, addr=%s, dev=%v)", *dataDir, *addr, *devMode)
+
+ // Open database (runs migrations automatically)
+ db, err := store.Open(*dataDir, *devMode)
+ if err != nil {
+ log.Fatalf("failed to open database: %v", err)
+ }
+ defer db.Close()
+
+ // Check first-run setup status
+ needsSetup, err := db.NeedsSetup()
+ if err != nil {
+ log.Fatalf("failed to check setup status: %v", err)
+ }
+ if needsSetup {
+ log.Printf("no operators found - first-run setup required")
+ log.Printf("POST /api/v1/setup to create first admin operator")
+ }
+
+ // Verify database is working
+ var one int
+ if err := db.QueryRow("SELECT 1").Scan(&one); err != nil {
+ log.Fatalf("database health check failed: %v", err)
+ }
+
+ log.Printf("database ready")
+
+ // Placeholder for HTTP server (will be implemented in Plan A)
+ _ = addr
+ fmt.Fprintf(os.Stderr, "felt: ready (database operational, HTTP server not yet implemented)\n")
+}
diff --git a/frontend/build/index.html b/frontend/build/index.html
new file mode 100644
index 0000000..ff8a382
--- /dev/null
+++ b/frontend/build/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ Felt
+
+
+
+
+
+
diff --git a/frontend/embed.go b/frontend/embed.go
new file mode 100644
index 0000000..04cc345
--- /dev/null
+++ b/frontend/embed.go
@@ -0,0 +1,38 @@
+package frontend
+
+import (
+ "embed"
+ "io/fs"
+ "net/http"
+)
+
+//go:embed all:build
+var files embed.FS
+
+// Handler returns an http.Handler that serves the embedded SvelteKit SPA.
+// Unknown paths fall back to index.html for client-side routing.
+func Handler() http.Handler {
+ fsys, _ := fs.Sub(files, "build")
+ fileServer := http.FileServer(http.FS(fsys))
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Try to serve static file first
+ path := r.URL.Path
+ if path == "/" {
+ path = "/index.html"
+ }
+
+ // Check if the file exists in the embedded filesystem
+ cleanPath := path[1:] // Remove leading slash
+ if cleanPath == "" {
+ cleanPath = "index.html"
+ }
+
+ if _, err := fs.Stat(fsys, cleanPath); err != nil {
+ // SPA fallback: serve index.html for client-side routing
+ r.URL.Path = "/"
+ }
+
+ fileServer.ServeHTTP(w, r)
+ })
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b5dad57
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,11 @@
+module github.com/felt-app/felt
+
+go 1.24.0
+
+require github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff // no tagged releases — pinned to commit 43644db490ff
+
+require (
+ github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+ github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect
+ golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..ede74d5
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,16 @@
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
+github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff h1:Hvxz9W8fWpSg9xkiq8/q+3cVJo+MmLMfkjdS/u4nWFY=
+github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
+golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
+golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/internal/audit/trail.go b/internal/audit/trail.go
new file mode 100644
index 0000000..6fd56fb
--- /dev/null
+++ b/internal/audit/trail.go
@@ -0,0 +1 @@
+package audit
diff --git a/internal/audit/undo.go b/internal/audit/undo.go
new file mode 100644
index 0000000..6fd56fb
--- /dev/null
+++ b/internal/audit/undo.go
@@ -0,0 +1 @@
+package audit
diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go
new file mode 100644
index 0000000..8832b06
--- /dev/null
+++ b/internal/auth/jwt.go
@@ -0,0 +1 @@
+package auth
diff --git a/internal/auth/pin.go b/internal/auth/pin.go
new file mode 100644
index 0000000..8832b06
--- /dev/null
+++ b/internal/auth/pin.go
@@ -0,0 +1 @@
+package auth
diff --git a/internal/blind/structure.go b/internal/blind/structure.go
new file mode 100644
index 0000000..d223211
--- /dev/null
+++ b/internal/blind/structure.go
@@ -0,0 +1 @@
+package blind
diff --git a/internal/blind/templates.go b/internal/blind/templates.go
new file mode 100644
index 0000000..d223211
--- /dev/null
+++ b/internal/blind/templates.go
@@ -0,0 +1 @@
+package blind
diff --git a/internal/blind/wizard.go b/internal/blind/wizard.go
new file mode 100644
index 0000000..d223211
--- /dev/null
+++ b/internal/blind/wizard.go
@@ -0,0 +1 @@
+package blind
diff --git a/internal/clock/engine.go b/internal/clock/engine.go
new file mode 100644
index 0000000..fa04f4c
--- /dev/null
+++ b/internal/clock/engine.go
@@ -0,0 +1 @@
+package clock
diff --git a/internal/clock/ticker.go b/internal/clock/ticker.go
new file mode 100644
index 0000000..fa04f4c
--- /dev/null
+++ b/internal/clock/ticker.go
@@ -0,0 +1 @@
+package clock
diff --git a/internal/clock/warnings.go b/internal/clock/warnings.go
new file mode 100644
index 0000000..fa04f4c
--- /dev/null
+++ b/internal/clock/warnings.go
@@ -0,0 +1 @@
+package clock
diff --git a/internal/financial/chop.go b/internal/financial/chop.go
new file mode 100644
index 0000000..6ced8cb
--- /dev/null
+++ b/internal/financial/chop.go
@@ -0,0 +1 @@
+package financial
diff --git a/internal/financial/engine.go b/internal/financial/engine.go
new file mode 100644
index 0000000..6ced8cb
--- /dev/null
+++ b/internal/financial/engine.go
@@ -0,0 +1 @@
+package financial
diff --git a/internal/financial/icm.go b/internal/financial/icm.go
new file mode 100644
index 0000000..6ced8cb
--- /dev/null
+++ b/internal/financial/icm.go
@@ -0,0 +1 @@
+package financial
diff --git a/internal/financial/payout.go b/internal/financial/payout.go
new file mode 100644
index 0000000..6ced8cb
--- /dev/null
+++ b/internal/financial/payout.go
@@ -0,0 +1 @@
+package financial
diff --git a/internal/financial/receipt.go b/internal/financial/receipt.go
new file mode 100644
index 0000000..6ced8cb
--- /dev/null
+++ b/internal/financial/receipt.go
@@ -0,0 +1 @@
+package financial
diff --git a/internal/nats/embedded.go b/internal/nats/embedded.go
new file mode 100644
index 0000000..40b4928
--- /dev/null
+++ b/internal/nats/embedded.go
@@ -0,0 +1 @@
+package nats
diff --git a/internal/nats/publisher.go b/internal/nats/publisher.go
new file mode 100644
index 0000000..40b4928
--- /dev/null
+++ b/internal/nats/publisher.go
@@ -0,0 +1 @@
+package nats
diff --git a/internal/player/player.go b/internal/player/player.go
new file mode 100644
index 0000000..5d9ce2c
--- /dev/null
+++ b/internal/player/player.go
@@ -0,0 +1 @@
+package player
diff --git a/internal/player/qrcode.go b/internal/player/qrcode.go
new file mode 100644
index 0000000..5d9ce2c
--- /dev/null
+++ b/internal/player/qrcode.go
@@ -0,0 +1 @@
+package player
diff --git a/internal/player/ranking.go b/internal/player/ranking.go
new file mode 100644
index 0000000..5d9ce2c
--- /dev/null
+++ b/internal/player/ranking.go
@@ -0,0 +1 @@
+package player
diff --git a/internal/seating/balance.go b/internal/seating/balance.go
new file mode 100644
index 0000000..63b7c6c
--- /dev/null
+++ b/internal/seating/balance.go
@@ -0,0 +1 @@
+package seating
diff --git a/internal/seating/blueprint.go b/internal/seating/blueprint.go
new file mode 100644
index 0000000..63b7c6c
--- /dev/null
+++ b/internal/seating/blueprint.go
@@ -0,0 +1 @@
+package seating
diff --git a/internal/seating/breaktable.go b/internal/seating/breaktable.go
new file mode 100644
index 0000000..63b7c6c
--- /dev/null
+++ b/internal/seating/breaktable.go
@@ -0,0 +1 @@
+package seating
diff --git a/internal/seating/table.go b/internal/seating/table.go
new file mode 100644
index 0000000..63b7c6c
--- /dev/null
+++ b/internal/seating/table.go
@@ -0,0 +1 @@
+package seating
diff --git a/internal/server/middleware/auth.go b/internal/server/middleware/auth.go
new file mode 100644
index 0000000..c870d7c
--- /dev/null
+++ b/internal/server/middleware/auth.go
@@ -0,0 +1 @@
+package middleware
diff --git a/internal/server/middleware/role.go b/internal/server/middleware/role.go
new file mode 100644
index 0000000..c870d7c
--- /dev/null
+++ b/internal/server/middleware/role.go
@@ -0,0 +1 @@
+package middleware
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000..abb4e43
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1 @@
+package server
diff --git a/internal/server/ws/hub.go b/internal/server/ws/hub.go
new file mode 100644
index 0000000..9859295
--- /dev/null
+++ b/internal/server/ws/hub.go
@@ -0,0 +1 @@
+package ws
diff --git a/internal/store/db.go b/internal/store/db.go
new file mode 100644
index 0000000..fc632bb
--- /dev/null
+++ b/internal/store/db.go
@@ -0,0 +1,130 @@
+// Package store provides database access for the Felt tournament engine.
+//
+// It manages the LibSQL (SQLite-compatible) connection, runs embedded migrations
+// on startup, and provides the database handle for use by other packages.
+package store
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+
+ _ "github.com/tursodatabase/go-libsql"
+)
+
+// DB wraps the underlying sql.DB connection with Felt-specific configuration.
+type DB struct {
+ *sql.DB
+ dataDir string
+}
+
+// Open opens (or creates) a LibSQL database in the given data directory.
+// It enables WAL mode and foreign key enforcement, then runs any pending
+// migrations automatically.
+func Open(dataDir string, devMode bool) (*DB, error) {
+ // Ensure data directory exists
+ if err := os.MkdirAll(dataDir, 0750); err != nil {
+ return nil, fmt.Errorf("store: create data dir: %w", err)
+ }
+
+ dbPath := filepath.Join(dataDir, "felt.db")
+ sqlDB, err := sql.Open("libsql", "file:"+dbPath)
+ if err != nil {
+ return nil, fmt.Errorf("store: open database: %w", err)
+ }
+
+ // Verify connection
+ if err := sqlDB.Ping(); err != nil {
+ sqlDB.Close()
+ return nil, fmt.Errorf("store: ping database: %w", err)
+ }
+
+ // Set PRAGMAs for performance and correctness.
+ // go-libsql: journal_mode returns a row, use QueryRow.
+ // foreign_keys and busy_timeout are set then verified with separate queries.
+ var walMode string
+ if err := sqlDB.QueryRow("PRAGMA journal_mode=WAL").Scan(&walMode); err != nil {
+ sqlDB.Close()
+ return nil, fmt.Errorf("store: set journal_mode: %w", err)
+ }
+ log.Printf("store: journal_mode=%s", walMode)
+
+ // These PRAGMAs are setters that don't return rows in go-libsql.
+ // We set them, then verify with a separate getter query.
+ if _, err := execPragma(sqlDB, "PRAGMA foreign_keys=ON"); err != nil {
+ sqlDB.Close()
+ return nil, fmt.Errorf("store: set foreign_keys: %w", err)
+ }
+ if _, err := execPragma(sqlDB, "PRAGMA busy_timeout=5000"); err != nil {
+ sqlDB.Close()
+ return nil, fmt.Errorf("store: set busy_timeout: %w", err)
+ }
+
+ // Verify settings
+ var fk, bt string
+ if err := sqlDB.QueryRow("PRAGMA foreign_keys").Scan(&fk); err == nil {
+ log.Printf("store: foreign_keys=%s", fk)
+ }
+ if err := sqlDB.QueryRow("PRAGMA busy_timeout").Scan(&bt); err == nil {
+ log.Printf("store: busy_timeout=%s", bt)
+ }
+
+ log.Printf("store: opened database at %s", dbPath)
+
+ db := &DB{
+ DB: sqlDB,
+ dataDir: dataDir,
+ }
+
+ // Run migrations
+ if err := RunMigrations(sqlDB, devMode); err != nil {
+ sqlDB.Close()
+ return nil, fmt.Errorf("store: run migrations: %w", err)
+ }
+
+ return db, nil
+}
+
+// execPragma tries Exec first; if that fails with "Execute returned rows",
+// it falls back to QueryRow. go-libsql is inconsistent about which PRAGMAs
+// return rows vs which don't.
+func execPragma(db *sql.DB, pragma string) (string, error) {
+ // Try Exec first (works for pure setters)
+ _, err := db.Exec(pragma)
+ if err == nil {
+ return "", nil
+ }
+ // If Exec fails because it returns rows, use QueryRow
+ var result string
+ if qErr := db.QueryRow(pragma).Scan(&result); qErr != nil {
+ // Return the original error if QueryRow also fails
+ return "", err
+ }
+ return result, nil
+}
+
+// DataDir returns the data directory path.
+func (db *DB) DataDir() string {
+ return db.dataDir
+}
+
+// OperatorCount returns the number of operators in the database.
+// Used by the first-run setup check.
+func (db *DB) OperatorCount() (int, error) {
+ var count int
+ err := db.QueryRow("SELECT COUNT(*) FROM operators").Scan(&count)
+ return count, err
+}
+
+// NeedsSetup returns true if there are zero operators and the system
+// needs initial admin setup. In dev mode, this is handled by the dev
+// seed migration, so it always returns false.
+func (db *DB) NeedsSetup() (bool, error) {
+ count, err := db.OperatorCount()
+ if err != nil {
+ return false, err
+ }
+ return count == 0, nil
+}
diff --git a/internal/store/migrate.go b/internal/store/migrate.go
new file mode 100644
index 0000000..087b222
--- /dev/null
+++ b/internal/store/migrate.go
@@ -0,0 +1,121 @@
+package store
+
+import (
+ "database/sql"
+ "embed"
+ "fmt"
+ "log"
+ "sort"
+ "strings"
+ "time"
+)
+
+//go:embed migrations/*.sql
+var migrationsFS embed.FS
+
+// devOnlyMigrations lists migration filenames that should only be applied
+// in development mode (--dev flag).
+var devOnlyMigrations = map[string]bool{
+ "004_dev_seed.sql": true,
+}
+
+// RunMigrations applies all pending SQL migrations embedded in the binary.
+// Migrations are sorted by filename (numeric prefix ensures order) and
+// executed within individual transactions. Each successful migration is
+// recorded in the _migrations table to prevent re-application.
+//
+// If devMode is false, migrations listed in devOnlyMigrations are skipped.
+func RunMigrations(db *sql.DB, devMode bool) error {
+ // Create migrations tracking table
+ _, err := db.Exec(`
+ CREATE TABLE IF NOT EXISTS _migrations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE,
+ applied_at INTEGER NOT NULL
+ )
+ `)
+ if err != nil {
+ return fmt.Errorf("create _migrations table: %w", err)
+ }
+
+ // Read all migration files from embedded filesystem
+ entries, err := migrationsFS.ReadDir("migrations")
+ if err != nil {
+ return fmt.Errorf("read migrations dir: %w", err)
+ }
+
+ // Sort by filename to ensure order
+ sort.Slice(entries, func(i, j int) bool {
+ return entries[i].Name() < entries[j].Name()
+ })
+
+ applied := 0
+ skipped := 0
+
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
+ continue
+ }
+
+ name := entry.Name()
+
+ // Skip dev-only migrations unless in dev mode
+ if devOnlyMigrations[name] && !devMode {
+ log.Printf("store: skipping dev-only migration %s (not in dev mode)", name)
+ continue
+ }
+
+ // Check if already applied
+ var count int
+ err := db.QueryRow("SELECT COUNT(*) FROM _migrations WHERE name = ?", name).Scan(&count)
+ if err != nil {
+ return fmt.Errorf("check migration %s: %w", name, err)
+ }
+ if count > 0 {
+ skipped++
+ continue
+ }
+
+ // Read migration SQL
+ sqlBytes, err := migrationsFS.ReadFile("migrations/" + name)
+ if err != nil {
+ return fmt.Errorf("read migration %s: %w", name, err)
+ }
+
+ // Execute migration within a transaction
+ tx, err := db.Begin()
+ if err != nil {
+ return fmt.Errorf("begin transaction for %s: %w", name, err)
+ }
+
+ if _, err := tx.Exec(string(sqlBytes)); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("execute migration %s: %w", name, err)
+ }
+
+ // Record migration as applied
+ now := time.Now().Unix()
+ if _, err := tx.Exec(
+ "INSERT INTO _migrations (name, applied_at) VALUES (?, ?)",
+ name, now,
+ ); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("record migration %s: %w", name, err)
+ }
+
+ if err := tx.Commit(); err != nil {
+ return fmt.Errorf("commit migration %s: %w", name, err)
+ }
+
+ applied++
+ log.Printf("store: applied migration %s", name)
+ }
+
+ if applied > 0 {
+ log.Printf("store: %d migration(s) applied, %d already up-to-date", applied, skipped)
+ } else if skipped > 0 {
+ log.Printf("store: all %d migration(s) already applied", skipped)
+ }
+
+ return nil
+}
diff --git a/internal/store/migrations/002_fts_indexes.sql b/internal/store/migrations/002_fts_indexes.sql
new file mode 100644
index 0000000..b37d2ec
--- /dev/null
+++ b/internal/store/migrations/002_fts_indexes.sql
@@ -0,0 +1,28 @@
+-- 002_fts_indexes.sql
+-- FTS5 virtual table for player typeahead search
+-- Synced with players table via triggers
+
+CREATE VIRTUAL TABLE IF NOT EXISTS players_fts USING fts5(
+ name, nickname, email,
+ content='players',
+ content_rowid='rowid'
+);
+
+-- Sync triggers: keep FTS index up to date with players table
+
+CREATE TRIGGER IF NOT EXISTS players_ai AFTER INSERT ON players BEGIN
+ INSERT INTO players_fts(rowid, name, nickname, email)
+ VALUES (new.rowid, new.name, new.nickname, new.email);
+END;
+
+CREATE TRIGGER IF NOT EXISTS players_ad AFTER DELETE ON players BEGIN
+ INSERT INTO players_fts(players_fts, rowid, name, nickname, email)
+ VALUES ('delete', old.rowid, old.name, old.nickname, old.email);
+END;
+
+CREATE TRIGGER IF NOT EXISTS players_au AFTER UPDATE ON players BEGIN
+ INSERT INTO players_fts(players_fts, rowid, name, nickname, email)
+ VALUES ('delete', old.rowid, old.name, old.nickname, old.email);
+ INSERT INTO players_fts(rowid, name, nickname, email)
+ VALUES (new.rowid, new.name, new.nickname, new.email);
+END;
diff --git a/internal/store/migrations/003_seed_data.sql b/internal/store/migrations/003_seed_data.sql
new file mode 100644
index 0000000..16bb01b
--- /dev/null
+++ b/internal/store/migrations/003_seed_data.sql
@@ -0,0 +1,27 @@
+-- 003_seed_data.sql
+-- Default venue settings and built-in chip sets
+-- Applied on every fresh database
+
+-- Default venue settings (DKK, Denmark)
+INSERT OR IGNORE INTO venue_settings (id, venue_name, currency_code, currency_symbol, rounding_denomination, receipt_mode, timezone)
+VALUES (1, '', 'DKK', 'kr', 5000, 'digital', 'Europe/Copenhagen');
+
+-- Standard chip set (universal denominations)
+INSERT OR IGNORE INTO chip_sets (id, name, is_builtin) VALUES (1, 'Standard', 1);
+
+INSERT OR IGNORE INTO chip_denominations (chip_set_id, value, color_hex, label, sort_order) VALUES
+ (1, 25, '#FFFFFF', '25', 1),
+ (1, 100, '#FF0000', '100', 2),
+ (1, 500, '#00AA00', '500', 3),
+ (1, 1000, '#000000', '1000', 4),
+ (1, 5000, '#0000FF', '5000', 5);
+
+-- Copenhagen chip set (DKK-friendly denominations)
+INSERT OR IGNORE INTO chip_sets (id, name, is_builtin) VALUES (2, 'Copenhagen', 1);
+
+INSERT OR IGNORE INTO chip_denominations (chip_set_id, value, color_hex, label, sort_order) VALUES
+ (2, 100, '#FFFFFF', '100', 1),
+ (2, 500, '#FF0000', '500', 2),
+ (2, 1000, '#00AA00', '1000', 3),
+ (2, 5000, '#000000', '5000', 4),
+ (2, 10000, '#0000FF', '10000', 5);
diff --git a/internal/store/migrations/004_dev_seed.sql b/internal/store/migrations/004_dev_seed.sql
new file mode 100644
index 0000000..361f610
--- /dev/null
+++ b/internal/store/migrations/004_dev_seed.sql
@@ -0,0 +1,12 @@
+-- 004_dev_seed.sql
+-- Development-only seed data: default admin operator
+-- Only applied when --dev flag is set
+-- PIN: 1234 (bcrypt hash below)
+
+INSERT OR IGNORE INTO operators (id, name, pin_hash, role)
+VALUES (
+ '00000000-0000-0000-0000-000000000001',
+ 'Admin',
+ '$2a$10$RD/9mCpMunyvJ0Ax1gHvHOrVmu0DL0/ah0NppuVN4rKgtpfuAEvdK',
+ 'admin'
+);
diff --git a/internal/template/buyin.go b/internal/template/buyin.go
new file mode 100644
index 0000000..38cdfe4
--- /dev/null
+++ b/internal/template/buyin.go
@@ -0,0 +1 @@
+package template
diff --git a/internal/template/chipset.go b/internal/template/chipset.go
new file mode 100644
index 0000000..38cdfe4
--- /dev/null
+++ b/internal/template/chipset.go
@@ -0,0 +1 @@
+package template
diff --git a/internal/template/payout.go b/internal/template/payout.go
new file mode 100644
index 0000000..38cdfe4
--- /dev/null
+++ b/internal/template/payout.go
@@ -0,0 +1 @@
+package template
diff --git a/internal/template/tournament.go b/internal/template/tournament.go
new file mode 100644
index 0000000..38cdfe4
--- /dev/null
+++ b/internal/template/tournament.go
@@ -0,0 +1 @@
+package template
diff --git a/internal/tournament/multi.go b/internal/tournament/multi.go
new file mode 100644
index 0000000..f4947cb
--- /dev/null
+++ b/internal/tournament/multi.go
@@ -0,0 +1 @@
+package tournament
diff --git a/internal/tournament/state.go b/internal/tournament/state.go
new file mode 100644
index 0000000..f4947cb
--- /dev/null
+++ b/internal/tournament/state.go
@@ -0,0 +1 @@
+package tournament
diff --git a/internal/tournament/tournament.go b/internal/tournament/tournament.go
new file mode 100644
index 0000000..f4947cb
--- /dev/null
+++ b/internal/tournament/tournament.go
@@ -0,0 +1 @@
+package tournament