Files
vikingowl a1d93f7a8e feat: implement MVP backend API
Go backend with Gin, pgx, Valkey (go-valkey), and PostGIS.

Domains:
- Market search with PostGIS geo-queries (ST_DWithin, ST_Distance),
  German full-text search (tsvector + ILIKE fallback for compound words),
  date range filtering, pagination, and slug-based detail endpoint
- Auth with email+password (bcrypt), JWT access tokens (15min),
  session tokens (30d, dual Valkey+Postgres storage), OAuth
  (Google/GitHub/Facebook), magic links, and TOTP 2FA
- User profile with CRUD, soft-delete (30d grace), and restore

Infrastructure:
- 6 database migrations (users, sessions, oauth_accounts, magic_links,
  markets with PostGIS+FTS, totp_secrets)
- Middleware: recovery, request ID, structured logging (slog), CORS,
  per-IP rate limiting, JWT auth
- Seed data: 10 medieval markets across DACH region
- Docker Compose (PostGIS 17 + Valkey 8), multi-stage Dockerfile,
  Woodpecker CI pipeline, Kubernetes manifests
- Justfile, golangci-lint config, env example
2026-02-18 05:52:20 +01:00

76 lines
1.4 KiB
Go

package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"marktvogt.de/backend/internal/config"
"marktvogt.de/backend/internal/database"
"marktvogt.de/backend/internal/server"
)
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})))
if err := run(); err != nil {
slog.Error("fatal error", "error", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load()
if err != nil {
return err
}
if cfg.IsDev() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
db, err := database.NewPostgres(ctx, cfg.DB)
if err != nil {
return err
}
defer db.Close()
vk, err := database.NewValkey(cfg.Valkey)
if err != nil {
return err
}
defer vk.Close()
srv := server.New(cfg, db, vk)
errCh := make(chan error, 1)
go func() {
errCh <- srv.Start()
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
select {
case err := <-errCh:
return err
case sig := <-quit:
slog.Info("received signal, shutting down", "signal", sig)
}
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer shutdownCancel()
return srv.Shutdown(shutdownCtx)
}