Architecture

Overview

Two services: a Go API (backend/) and a React SPA (frontend/). They communicate over HTTP, the frontend calls /api/v1/* endpoints. In Docker, only the frontend port is published while the backend is internal.

Backend

Project Structure

backend/
  main.go              # Init: logger, config, DB, scheduler, router, graceful shutdown
  config/              # Environment variable loading and validation
  routes/routes.go     # All route registrations
  middleware/          # Auth, rate limiting, validation, logging, request ID
  controllers/         # HTTP handlers — thin, delegate to services or query DB directly
  models/              # GORM models and input DTOs
  services/            # Business logic (reminders, import, birthdays, password reset)
  errors/              # AppError type and error handler middleware
  database/migrations/ # Embedded SQL migrations, auto-applied on startup
  carddav/             # CardDAV protocol implementation
  i18n/                # Backend translations (email notifications)

Error Handling

Controllers return *apperrors.AppError. The error handler middleware at the top of the stack catches these and writes a structured JSON response. Unhandled panics are also caught.

return nil, apperrors.New(http.StatusNotFound, "CONTACT_NOT_FOUND", "contact not found")

Database Layer

SQLite via GORM. All SQL migrations live in database/migrations/ as embedded files and run automatically on startup in version order. Use make migrate-create NAME=xxx to add a new migration.

Every table includes user_id for multi-tenant isolation and all queries must filter by it.

Frontend

Project Structure

frontend/src/
  api/          # One module per resource (contacts, activities, …) + client.ts
  hooks/        # Data-fetching hooks (useContacts, useActivities, …)
  components/   # Reusable dialog and display components
  pages/        # Top-level route components (one per page)
  types/        # Shared TypeScript types
  i18n/locales/ # en.json, de.json

State Management

No global store. Each page owns its state via custom hooks. Hooks encapsulate API calls and expose { data, loading, error, refetch }. Dialogs manage local open/close state in useContactDialogs and similar hooks.

Routing

React Router. Each page component maps to a route. Protected routes redirect to /login on 401 (handled automatically in api/client.ts).

Internationalization

i18next with react-i18next. All UI strings live in i18n/locales/en.json and de.json. Use the useTranslation() hook. The user’s language preference is stored server-side and applied to backend email notifications too.

Security

Authentication — JWT (HMAC-signed). Tokens are stored in an httpOnly cookie for the browser; API clients (e.g. CardDAV) use a Bearer header fallback. Tokens carry user_id and username claims and expire automatically (expiration set by env variable).

Multi-tenant isolation — every table has a user_id column; all queries filter by the ID extracted from the validated JWT, so one user cannot access another’s data.

Password policy — entropy-based validation (minimum 50 bits). Passwords are rejected at registration/change time before they are hashed.

Account lockout — after 5 failed login attempts the account is locked with exponential back-off (1 min base, doubling up to 30 min). The lockout state is reset after 1 hour of inactivity.

Rate limiting — per-IP token-bucket limiter applied globally; tighter limits on auth endpoints.

Request size limits — JSON bodies capped at 1 MB, other bodies at 10 MB to prevent DoS via large payloads.


Back to top

Meerkat CRM - Self-hosted personal contact management

This site uses Just the Docs, a documentation theme for Jekyll.