Skip to content

Architecture & Project Structure

Multi-Service Monorepo

RestartiX is a Go monorepo containing two services deployed as separate AWS App Runner services:

ServicePortFrameworkDatabasesPurpose
Core API (cmd/api)9000chi (net/http)PostgreSQL + RedisCore platform: appointments, patients, forms, RBAC, RLS
Telemetry API (cmd/telemetry)4000chi (net/http)PostgreSQL + ClickHouse + RedisAudit enrichment, analytics, media/rehab tracking

Why chi for both: chi builds on Go's net/http standard library, which means middleware and handlers are portable across services. The monorepo shares types via pkg/ — audit event types are defined once and imported by both services. Using two different frameworks (e.g., chi + Fiber) would prevent middleware sharing.

Why two services, not one: Telemetry ingests high-frequency data (pose tracking, video bandwidth samples, analytics events) that would compete for resources with the Core API's CRUD operations. Separate services allow independent scaling — rehab sessions spike at different times than admin operations. ClickHouse (columnar, analytics-optimized) doesn't belong in the Core API's request path.

Communication: The Core API forwards audit events to Telemetry via the TELEMETRY_INTERNAL_URL environment variable (configured in AWS Secrets Manager, pointing to the Telemetry App Runner service URL over HTTPS). Both services connect to RDS and ElastiCache through a VPC Connector. Frontend client SDK talks to Telemetry directly for analytics and media tracking. See AWS infrastructure for networking details.

AWS App Runner + VPC
├── Core API (App Runner, chi, :8080)
│   └── RDS PostgreSQL (Core DB — appointments, patients, forms, audit_log)
│   └── ElastiCache Redis (shared — rate limiting, sessions, webhook idempotency)

├── Telemetry API (App Runner, chi, :4000)
│   └── RDS PostgreSQL (Telemetry DB — enriched audit, security events, staff activity)
│   └── ClickHouse (analytics, media sessions, bandwidth, staff metrics)
│   └── ElastiCache Redis (shared)

├── Core API → Telemetry (HTTPS via TELEMETRY_INTERNAL_URL)

└── Frontend → Core API (mutations, queries)
    Frontend → Telemetry API (analytics/track, media/events)

Go Project Layout

restartix/
├── go.mod                                # Single module
├── go.sum

├── cmd/
│   ├── api/
│   │   └── main.go                       # Core API entrypoint (port 9000)
│   └── telemetry/
│       └── main.go                       # Telemetry API entrypoint (port 4000)

├── pkg/                                  # Shared types between services
│   ├── auditevt/                         # Audit event types (Core API produces, Telemetry consumes)
│   │   ├── event.go                      # Event struct, action types
│   │   └── forward.go                    # Async forwarder (used by Core API)
│   └── httputil/                         # Shared HTTP response helpers
│       └── response.go

├── internal/                             # Private application code
│   ├── core/                              # Core API code
│   │   ├── config/
│   │   │   └── config.go                 # Core API environment configuration
│   │   ├── server/
│   │   │   ├── server.go                 # HTTP server setup, graceful shutdown
│   │   │   └── routes.go                 # Route registration
│   │   ├── middleware/
│   │   │   ├── auth.go                   # Clerk session validation → user context
│   │   │   ├── organization.go           # Organization context (RLS session var)
│   │   │   ├── rbac.go                   # Role-based access control
│   │   │   ├── audit.go                   # Audit: sync local INSERT + async forward to Telemetry
│   │   │   ├── ratelimit.go              # Per-endpoint rate limiting (Redis)
│   │   │   ├── security.go               # Security headers, attack blocking
│   │   │   ├── requestid.go              # Request ID propagation
│   │   │   ├── logging.go               # Request/response structured logging
│   │   │   └── recovery.go               # Panic recovery
│   │
│   │   ├── domain/                    # Core business domain (entities + logic)
│   │   │   ├── user/
│   │   │   │   ├── model.go           # User, UserRole types
│   │   │   │   ├── repository.go      # Database queries
│   │   │   │   ├── service.go         # Business logic
│   │   │   │   ├── handler.go         # HTTP handlers
│   │   │   │   └── errors.go          # Domain-specific errors
│   │   │   ├── organization/
│   │   │   ├── appointment/           # CRUD + workflows (createFromTemplate, book)
│   │   │   ├── patient/               # Onboarding, profile management
│   │   │   ├── specialist/
│   │   │   ├── specialty/
│   │   │   ├── form/                  # Forms feature: instance lifecycle, value management, signing
│   │   │   ├── formtemplate/          # Forms feature: template CRUD, versioning, publishing
│   │   │   ├── document/              # Reports + prescriptions
│   │   │   ├── customfield/           # Versioned field library (referenced by forms, segments)
│   │   │   ├── segment/              # Rules-based patient groups
│   │   │   └── export/                # CSV export with security filters
│   │   │
│   │   ├── integration/               # Core API's external service clients
│   │   │   ├── dailyco/               # Daily.co videocall API
│   │   │   ├── s3/                    # AWS S3 file operations
│   │   │   └── webhook/               # Webhook emitter + delivery worker
│   │   │
│   │   └── database/
│   │       ├── postgres.go            # Connection pool setup, RLS session config
│   │       ├── tx.go                  # Transaction helpers
│   │       └── querier.go             # Querier interface (ConnFromContext)
│   │
│   ├── telemetry/                     # Telemetry-specific code
│   │   ├── config/
│   │   │   └── config.go              # Telemetry environment configuration
│   │   ├── server/
│   │   │   ├── server.go
│   │   │   └── routes.go              # /v1/audit/ingest, /v1/analytics/track, /v1/media/events, /v1/pose/frames
│   │   ├── middleware/
│   │   │   └── compliance.go          # IP extraction, actor hashing, consent, geo enrichment
│   │   ├── audit/                     # Audit ingestion + enrichment
│   │   ├── analytics/                 # General analytics tracking
│   │   ├── media/                     # Video/rehab tracking with bandwidth
│   │   ├── staff/                     # Staff activity tracking
│   │   ├── storage/
│   │   │   ├── postgres/              # Telemetry PostgreSQL client (audit.audit_logs, security.security_events)
│   │   │   └── clickhouse/            # ClickHouse client (analytics_events, media_sessions)
│   │   └── privacy/                   # CCPA/privacy exclusions
│   │
│   └── shared/                        # Shared internal utilities
│       ├── crypto/
│       │   └── encryption.go          # AES-256-GCM encrypt/decrypt for PHI
│       ├── httputil/
│       │   ├── response.go            # JSON response helpers
│       │   ├── request.go             # Request parsing, pagination
│       │   └── errors.go              # HTTP error types and rendering
│       ├── validate/
│       │   └── validate.go            # Validation setup and helpers
│       └── slug/
│           └── slug.go                # Slug generation utility

├── migrations/
│   ├── core/                            # Core API PostgreSQL migrations
│   ├── telemetry-pg/                  # Telemetry PostgreSQL migrations (5 schemas)
│   └── telemetry-ch/                  # Telemetry ClickHouse migrations

├── Dockerfile
├── docker-compose.yml
├── Makefile                           # Build, test, migrate, lint commands
├── go.mod
├── go.sum
├── .env.example
└── README.md

Core Patterns

1. Dependency Injection via Constructors

No DI framework. Services receive their dependencies through constructors.

go
// internal/domain/appointment/service.go
type Service struct {
    repo          *Repository
    formService   *form.Service
    schedService  *SchedulingService
    auditClient   *audit.Client
}

func NewService(
    repo *Repository,
    formService *form.Service,
    schedService *SchedulingService,
    auditClient *audit.Client,
) *Service {
    return &Service{
        repo:          repo,
        formService:   formService,
        schedService:  schedService,
        auditClient:   auditClient,
    }
}

2. Repository Pattern

Each domain has a repository that owns all SQL queries. No SQL leaks into services or handlers.

Repositories use the RLS-configured connection from context, not the pool directly. The OrganizationContext middleware acquires a connection, sets RLS session variables, and stores it in the request context. Repositories extract this connection to ensure all queries run with the correct tenant scope.

go
// internal/domain/appointment/repository.go
type Repository struct {
    pool *pgxpool.Pool // fallback for background jobs (superadmin, no RLS)
}

func NewRepository(pool *pgxpool.Pool) *Repository {
    return &Repository{pool: pool}
}

// connFromContext extracts the RLS-configured connection set by OrganizationContext middleware.
// Falls back to pool for background jobs where no request context exists.
func (r *Repository) connFromContext(ctx context.Context) database.Querier {
    if conn := database.ConnFromContext(ctx); conn != nil {
        return conn // RLS session variables already set on this connection
    }
    return r.pool // Background job — no RLS (must be superadmin/migration context)
}

// RLS handles organization scoping automatically.
// The connection already has app.current_org_id set via middleware.
func (r *Repository) FindByID(ctx context.Context, id int64) (*Appointment, error) {
    conn := r.connFromContext(ctx)
    query := `
        SELECT id, title, uid, status, started_at, ended_at,
               specialist_id, organization_id, user_id,
               appointment_template_id, specialty_id,
               created_at, updated_at
        FROM appointments
        WHERE id = $1
    `
    // RLS will automatically filter by organization
    var a Appointment
    err := conn.QueryRow(ctx, query, id).Scan(...)
    if err != nil {
        return nil, err
    }
    return &a, nil
}

3. Handler Pattern (Thin HTTP Layer)

Handlers only do: parse request → call service → render response.

go
// internal/domain/appointment/handler.go
type Handler struct {
    service *Service
}

func NewHandler(service *Service) *Handler {
    return &Handler{service: service}
}

func (h *Handler) CreateFromTemplate(w http.ResponseWriter, r *http.Request) {
    var req CreateFromTemplateRequest
    if err := httputil.DecodeJSON(r, &req); err != nil {
        httputil.BadRequest(w, err)
        return
    }
    if err := httputil.Validate(&req); err != nil {
        httputil.ValidationError(w, err)
        return
    }

    userCtx := middleware.UserFromContext(r.Context())

    appointment, err := h.service.CreateFromTemplate(r.Context(), userCtx, &req)
    if err != nil {
        httputil.HandleError(w, err)
        return
    }

    httputil.JSON(w, http.StatusCreated, appointment)
}

4. Error Handling

Domain errors are typed. The HTTP layer maps them to status codes.

go
// internal/domain/appointment/errors.go
package appointment

import "restartix-api/internal/pkg/httputil"

var (
    ErrNotFound             = httputil.NewNotFoundError("appointment", "Appointment not found")
    ErrTemplateNotFound     = httputil.NewNotFoundError("appointment_template", "Appointment template not found")
    ErrAlreadyCancelled     = httputil.NewConflictError("already_cancelled", "Appointment is already cancelled")
)

// internal/pkg/httputil/errors.go
type AppError struct {
    Code       string `json:"code"`               // Machine-readable: "appointment_not_found"
    Message    string `json:"message"`             // Human-readable
    StatusCode int    `json:"-"`                   // HTTP status
    Details    any    `json:"details,omitempty"`    // Optional extra data
}

func (e *AppError) Error() string { return e.Message }

func NewNotFoundError(code, message string) *AppError {
    return &AppError{Code: code, Message: message, StatusCode: 404}
}

func NewBadRequestError(code, message string) *AppError {
    return &AppError{Code: code, Message: message, StatusCode: 400}
}

func NewConflictError(code, message string) *AppError {
    return &AppError{Code: code, Message: message, StatusCode: 409}
}

func NewForbiddenError(code, message string) *AppError {
    return &AppError{Code: code, Message: message, StatusCode: 403}
}

func NewServiceError(code, message string) *AppError {
    return &AppError{Code: code, Message: message, StatusCode: 502}
}

// HandleError maps domain errors to HTTP responses
func HandleError(w http.ResponseWriter, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        JSON(w, appErr.StatusCode, map[string]any{
            "error": appErr,
        })
        return
    }
    // Unknown error → 500, don't leak internals
    slog.Error("unexpected error", "error", err)
    JSON(w, 500, map[string]any{
        "error": &AppError{
            Code:    "internal_error",
            Message: "An unexpected error occurred",
        },
    })
}

5. Middleware Pipeline

go
// internal/server/routes.go
func (s *Server) routes() http.Handler {
    r := chi.NewRouter()

    // Global middleware (order matters)
    r.Use(middleware.RequestID)
    r.Use(middleware.Recovery)
    r.Use(middleware.SecurityHeaders)
    r.Use(middleware.Logging)
    r.Use(middleware.AttackBlocker)      // Block /.env, /wp-admin, etc.

    // Health check (no auth required)
    r.Get("/health", s.healthHandler.Check)

    // Clerk webhook (verified by Clerk signature, not session)
    r.Post("/webhooks/clerk", s.userHandler.ClerkWebhook)

    // Authenticated routes
    r.Group(func(r chi.Router) {
        r.Use(middleware.ClerkAuth(s.clerkClient))     // Validate Clerk session → user context
        r.Use(middleware.OrganizationContext(s.adminPool, s.appPool))  // Route to pool, set RLS session vars
        r.Use(middleware.AuditLog(s.auditClient))       // Audit all mutations

        // User / Auth
        r.Get("/v1/me", s.userHandler.Me)
        r.Put("/v1/me/switch-organization", s.userHandler.SwitchOrganization)

        // Organizations
        r.Route("/v1/organizations", func(r chi.Router) {
            r.Get("/", s.orgHandler.List)
            r.Post("/", s.orgHandler.Create)           // superadmin only
            r.Get("/slug/{slug}", s.orgHandler.FindBySlug)

            r.Route("/{orgID}", func(r chi.Router) {
                r.Use(middleware.RequireOrgMember)
                r.Get("/", s.orgHandler.Get)
                r.Put("/", s.orgHandler.Update)
                r.Get("/api-keys", s.orgHandler.GetAPIKeys)
                r.Post("/connect-user", s.orgHandler.ConnectUser)
            })
        })

        // Appointments
        r.Route("/v1/appointments", func(r chi.Router) {
            r.Get("/", s.appointmentHandler.List)
            r.Post("/", s.appointmentHandler.Create)
            r.Post("/from-template", s.appointmentHandler.CreateFromTemplate)

            r.Route("/{appointmentID}", func(r chi.Router) {
                r.Get("/", s.appointmentHandler.Get)
                r.Put("/", s.appointmentHandler.Update)
                r.Delete("/", s.appointmentHandler.Delete)
                r.Post("/attach-forms", s.appointmentHandler.AttachForms)
                r.Post("/reschedule", s.appointmentHandler.Reschedule)
                r.Post("/cancel", s.appointmentHandler.Cancel)
            })

            r.Get("/uid/{uid}", s.appointmentHandler.FindByUID)
        })

        // Calendar (separate path for clarity)
        r.Get("/v1/calendar", s.appointmentHandler.CalendarView)
        r.Post("/v1/calendar", s.appointmentHandler.CalendarView)

        // Patients
        r.Route("/v1/patients", func(r chi.Router) {
            r.Get("/", s.patientHandler.List)
            r.Post("/", s.patientHandler.Create)
            r.Post("/onboard", s.patientHandler.Onboard)

            r.Route("/{patientID}", func(r chi.Router) {
                r.Get("/", s.patientHandler.Get)
                r.Put("/", s.patientHandler.Update)
                r.Delete("/", s.patientHandler.Delete)
                r.Post("/impersonate", s.patientHandler.Impersonate) // admin only
                r.Get("/profile", s.customFieldHandler.GetProfile)   // custom field values
                r.Put("/profile", s.customFieldHandler.UpdateProfile)
                r.Get("/prefill", s.customFieldHandler.GetPrefill)   // form auto-fill
                r.Get("/segments", s.segmentHandler.ListForPatient)
            })
        })

        // Specialists
        r.Route("/v1/specialists", func(r chi.Router) {
            r.Get("/", s.specialistHandler.List)
            r.Post("/", s.specialistHandler.Create)

            r.Route("/{specialistID}", func(r chi.Router) {
                r.Get("/", s.specialistHandler.Get)
                r.Put("/", s.specialistHandler.Update)
                r.Delete("/", s.specialistHandler.Delete)
                r.Get("/profile", s.customFieldHandler.GetProfile)   // custom field values
                r.Put("/profile", s.customFieldHandler.UpdateProfile)
            })
        })

        // Specialties
        r.Route("/v1/specialties", func(r chi.Router) {
            r.Get("/", s.specialtyHandler.List)
            r.Post("/", s.specialtyHandler.Create)
            r.Route("/{specialtyID}", func(r chi.Router) {
                r.Get("/", s.specialtyHandler.Get)
                r.Put("/", s.specialtyHandler.Update)
                r.Delete("/", s.specialtyHandler.Delete)
            })
        })

        // Appointment Templates
        r.Route("/v1/appointment-templates", func(r chi.Router) {
            r.Get("/", s.templateHandler.List)
            r.Post("/", s.templateHandler.Create)
            r.Get("/slug/{slug}", s.templateHandler.FindBySlug)
            r.Route("/{templateID}", func(r chi.Router) {
                r.Get("/", s.templateHandler.Get)
                r.Put("/", s.templateHandler.Update)
                r.Delete("/", s.templateHandler.Delete)
            })
        })

        // Forms (includes templates and instances - unified feature)
        r.Route("/v1/forms", func(r chi.Router) {
            r.Get("/", s.formHandler.List)
            r.Post("/", s.formHandler.Create)
            r.Route("/{formID}", func(r chi.Router) {
                r.Get("/", s.formHandler.Get)
                r.Put("/", s.formHandler.Update)
                r.Delete("/", s.formHandler.Delete)
                r.Post("/sign", s.formHandler.Sign)
            })
        })

        // Form Templates (part of forms feature - design-time templates)
        r.Route("/v1/form-templates", func(r chi.Router) {
            r.Get("/", s.formTemplateHandler.List)
            r.Post("/", s.formTemplateHandler.Create)
            r.Route("/{templateID}", func(r chi.Router) {
                r.Get("/", s.formTemplateHandler.Get)
                r.Put("/", s.formTemplateHandler.Update)
                r.Delete("/", s.formTemplateHandler.Delete)
                r.Post("/publish", s.formTemplateHandler.Publish)
                r.Get("/versions", s.formTemplateHandler.ListVersions)
            })
        })

        // Reports (appointment_documents where type=report)
        r.Route("/v1/reports", func(r chi.Router) {
            r.Get("/", s.documentHandler.ListReports)
            r.Post("/", s.documentHandler.CreateReport)
            r.Route("/{documentID}", func(r chi.Router) {
                r.Get("/", s.documentHandler.Get)
                r.Put("/", s.documentHandler.Update)
                r.Delete("/", s.documentHandler.Delete)
            })
        })

        // Prescriptions (appointment_documents where type=prescription)
        r.Route("/v1/prescriptions", func(r chi.Router) {
            r.Get("/", s.documentHandler.ListPrescriptions)
            r.Post("/", s.documentHandler.CreatePrescription)
            r.Route("/{documentID}", func(r chi.Router) {
                r.Get("/", s.documentHandler.Get)
                r.Put("/", s.documentHandler.Update)
                r.Delete("/", s.documentHandler.Delete)
            })
        })

        // Custom Fields
        r.Route("/v1/custom-fields", func(r chi.Router) {
            r.Get("/", s.customFieldHandler.List)
            r.Post("/", s.customFieldHandler.Create)
            r.Route("/{fieldID}", func(r chi.Router) {
                r.Get("/", s.customFieldHandler.Get)
                r.Put("/", s.customFieldHandler.Update)
                r.Delete("/", s.customFieldHandler.Delete)
            })
        })

        // Segments
        r.Route("/v1/segments", func(r chi.Router) {
            r.Get("/", s.segmentHandler.List)
            r.Post("/", s.segmentHandler.Create)
            r.Route("/{segmentID}", func(r chi.Router) {
                r.Get("/", s.segmentHandler.Get)
                r.Put("/", s.segmentHandler.Update)
                r.Delete("/", s.segmentHandler.Delete)
                r.Get("/members", s.segmentHandler.ListMembers)
                r.Post("/evaluate", s.segmentHandler.Evaluate)
            })
        })

        // Export
        r.With(middleware.RateLimit(5, 5*time.Minute)).
            Post("/v1/export/csv", s.exportHandler.CSV)

        // GDPR endpoints
        r.Route("/v1/gdpr", func(r chi.Router) {
            r.Use(middleware.RequireRole("superadmin", "admin"))
            r.Get("/users/{userID}/export", s.userHandler.GDPRExport)
            r.Delete("/users/{userID}", s.userHandler.GDPRDelete)
            r.Post("/users/{userID}/anonymize", s.userHandler.GDPRAnonymize)
        })
    })

    return r
}

6. Configuration

go
// internal/config/config.go
type Config struct {
    // Server
    Host string `envconfig:"HOST" default:"0.0.0.0"`
    Port int    `envconfig:"PORT" default:"8080"`

    // Database — Two-pool architecture for defense-in-depth:
    // AdminPool (DATABASE_URL): owner role, bypasses RLS. Used by auth middleware, superadmins, system queries.
    // AppPool (DATABASE_APP_URL): restricted role, RLS enforced. Used by public, staff, patient requests.
    // Pool sizing: each request holds a connection for its full duration (RLS requires it).
    // DB_POOL_MAX must be >= expected peak concurrent requests.
    DatabaseURL          string        `envconfig:"DATABASE_URL" required:"true"`
    DatabaseAppURL       string        `envconfig:"DATABASE_APP_URL"`             // Falls back to DATABASE_URL if empty
    DBPoolMin            int           `envconfig:"DB_POOL_MIN" default:"5"`
    DBPoolMax            int           `envconfig:"DB_POOL_MAX" default:"25"`
    DBPoolMaxConnLifetime time.Duration `envconfig:"DB_POOL_MAX_CONN_LIFETIME" default:"1h"`
    DBPoolMaxConnIdleTime time.Duration `envconfig:"DB_POOL_MAX_CONN_IDLE_TIME" default:"30m"`

    // Clerk Auth
    ClerkSecretKey    string `envconfig:"CLERK_SECRET_KEY" required:"true"`
    ClerkWebhookSecret string `envconfig:"CLERK_WEBHOOK_SECRET" required:"true"`

    // Redis (rate limiting, session tracking)
    RedisURL string `envconfig:"REDIS_URL" required:"true"`

    // Encryption
    EncryptionKey string `envconfig:"ENCRYPTION_KEY" required:"true"` // 64 hex chars (32 bytes)

    // AWS S3
    AWSAccessKeyID     string `envconfig:"AWS_ACCESS_KEY_ID" required:"true"`
    AWSSecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY" required:"true"`
    AWSRegion          string `envconfig:"AWS_REGION" required:"true"`
    AWSBucketName      string `envconfig:"AWS_BUCKET_NAME" required:"true"`

    // Daily.co
    DailyAPIKey    string `envconfig:"DAILY_API_KEY" required:"true"`
    DailyRESTDomain string `envconfig:"DAILY_REST_DOMAIN" default:"https://api.daily.co/v1"`

    // Telemetry Service (sibling App Runner service — audit forwarding)
    // In production, set via AWS Secrets Manager to the Telemetry App Runner service URL
    TelemetryInternalURL string `envconfig:"TELEMETRY_INTERNAL_URL" required:"true"`

    // App
    AppDomain string `envconfig:"APP_DOMAIN" required:"true"`
    LogLevel  string `envconfig:"LOG_LEVEL" default:"info"`
}

7. Database Connection with RLS (Two-Pool Architecture)

The Core API uses two connection pools:

  • AdminPool (DATABASE_URL): Owner role (restartix), bypasses RLS. Used by auth middleware (user lookup), superadmin requests, and system queries.
  • AppPool (DATABASE_APP_URL): Restricted role (restartix_app), RLS enforced. Used by all org-scoped, patient, and public requests.
go
// internal/database/postgres.go
package database

import (
    "context"
    "fmt"

    "github.com/jackc/pgx/v5/pgxpool"
)

func NewPool(ctx context.Context, databaseURL string, minConns, maxConns int) (*pgxpool.Pool, error) {
    config, err := pgxpool.ParseConfig(databaseURL)
    if err != nil {
        return nil, fmt.Errorf("parse database config: %w", err)
    }

    config.MinConns = int32(minConns)
    config.MaxConns = int32(maxConns)

    // After each connection is acquired from pool, set RLS session variables.
    // This is handled per-request in the middleware, not here.

    pool, err := pgxpool.NewWithConfig(ctx, config)
    if err != nil {
        return nil, fmt.Errorf("create connection pool: %w", err)
    }

    if err := pool.Ping(ctx); err != nil {
        return nil, fmt.Errorf("ping database: %w", err)
    }

    return pool, nil
}

// SetRLSContext sets session variables for Row-Level Security.
// Called at the start of every authenticated request.
func SetRLSContext(ctx context.Context, conn interface{ Exec(ctx context.Context, sql string, args ...any) (any, error) }, userID int64, orgID int64, role string) error {
    // These variables are read by RLS policies
    _, err := conn.Exec(ctx, `
        SELECT set_config('app.current_user_id', $1::text, true),
               set_config('app.current_org_id', $2::text, true),
               set_config('app.current_role', $3, true)
    `, userID, orgID, role)
    return err
}

8. Server Entrypoint

go
// cmd/server/main.go
package main

import (
    "context"
    "log/slog"
    "os"
    "os/signal"
    "syscall"

    "restartix-api/internal/config"
    "restartix-api/internal/database"
    "restartix-api/internal/server"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    cfg, err := config.Load()
    if err != nil {
        slog.Error("failed to load config", "error", err)
        os.Exit(1)
    }

    setupLogging(cfg.LogLevel)

    db, err := database.NewPool(ctx, cfg.DatabaseURL, cfg.DBPoolMin, cfg.DBPoolMax)
    if err != nil {
        slog.Error("failed to connect to database", "error", err)
        os.Exit(1)
    }
    defer db.Close()

    srv := server.New(cfg, db)
    if err := srv.Run(ctx); err != nil {
        slog.Error("server error", "error", err)
        os.Exit(1)
    }
}

func setupLogging(level string) {
    var lvl slog.Level
    switch level {
    case "debug":
        lvl = slog.LevelDebug
    case "warn":
        lvl = slog.LevelWarn
    case "error":
        lvl = slog.LevelError
    default:
        lvl = slog.LevelInfo
    }
    slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl})))
}

9. Request/Response Utilities

go
// internal/pkg/httputil/response.go
package httputil

import (
    "encoding/json"
    "net/http"
)

func JSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func NoContent(w http.ResponseWriter) {
    w.WriteHeader(http.StatusNoContent)
}

// Paginated response wrapper
type PaginatedResponse[T any] struct {
    Data []T  `json:"data"`
    Meta Meta `json:"meta"`
}

type Meta struct {
    Page       int   `json:"page"`
    PageSize   int   `json:"page_size"`
    Total      int64 `json:"total"`
    TotalPages int   `json:"total_pages"`
}

// internal/pkg/httputil/request.go
func DecodeJSON(r *http.Request, dst any) error {
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    return dec.Decode(dst)
}

func PaginationFromRequest(r *http.Request) (page, pageSize int) {
    // Parse ?page=1&page_size=25 with sane defaults
    page = intQueryParam(r, "page", 1)
    pageSize = intQueryParam(r, "page_size", 25)
    if pageSize > 100 {
        pageSize = 100 // Hard cap
    }
    if page < 1 {
        page = 1
    }
    return page, pageSize
}

10. Makefile

makefile
.PHONY: build-api build-telemetry run-api run-telemetry test lint migrate-core-up migrate-telemetry-up

# Core API
build-api:
	go build -o bin/api ./cmd/api

run-api:
	go run ./cmd/api

# Telemetry API
build-telemetry:
	go build -o bin/telemetry ./cmd/telemetry

run-telemetry:
	go run ./cmd/telemetry

# Shared
test:
	go test ./... -v -race -count=1

test-cover:
	go test ./... -coverprofile=coverage.out
	go tool cover -html=coverage.out -o coverage.html

lint:
	golangci-lint run ./...

# Migrations
migrate-core-up:
	migrate -path migrations/core -database "$(DATABASE_URL)" up

migrate-core-down:
	migrate -path migrations/core -database "$(DATABASE_URL)" down 1

migrate-telemetry-pg-up:
	migrate -path migrations/telemetry-pg -database "$(TELEMETRY_DATABASE_URL)" up

migrate-telemetry-ch-up:
	migrate -path migrations/telemetry-ch -database "$(CLICKHOUSE_URL)" up

migrate-create-core:
	migrate create -ext sql -dir migrations/core -seq $(name)

docker-build-api:
	docker build -f deploy/Dockerfile.api -t restartix-api .

docker-build-telemetry:
	docker build -f deploy/Dockerfile.telemetry -t restartix-telemetry .

11. Dockerfiles

Each service has its own Dockerfile. Both build from the monorepo root.

deploy/Dockerfile.api

dockerfile
# Build stage
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/api ./cmd/api

# Runtime stage
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
RUN adduser -D -g '' appuser
COPY --from=builder /bin/api /bin/api
COPY --from=builder /app/migrations/core /migrations
USER appuser
EXPOSE 9000
ENTRYPOINT ["/bin/api"]

deploy/Dockerfile.telemetry

dockerfile
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/telemetry-api ./cmd/telemetry-api

FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
RUN adduser -D -g '' appuser
COPY --from=builder /bin/telemetry-api /bin/telemetry-api
COPY --from=builder /app/migrations/telemetry-pg /migrations/pg
COPY --from=builder /app/migrations/telemetry-ch /migrations/ch
USER appuser
EXPOSE 4000
ENTRYPOINT ["/bin/telemetry-api"]