ultisuite-backend/README.md
R3D347HR4Y 38c0534012
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
fix(docs): update README and Nginx configuration for frontend routes
- Corrected frontend route references in README to reflect updated paths for account and settings.
- Modified Nginx configuration comments to align with the new route structure, ensuring clarity for development and production setups.
- Added new Nginx location blocks for handling account settings and redirecting old paths to the new structure.
2026-06-16 11:32:56 +02:00

12 KiB
Raw Permalink Blame History

Ulti Backend

Backend monolithe Go orchestrant la Ulti Suite — alternative souveraine à Google Suite.

Architecture

┌─────────────────────────────────────────────────────────┐
│  nginx (reverse proxy unique, port 80)                  │
├─────────────────────────────────────────────────────────┤
│  ultid (Go monolithe)                                   │
│  ├─ /api/v1/mail      — Ultimail (IMAP/SMTP custom)    │
│  ├─ /api/v1/drive     — Ultidrive (→ Nextcloud WebDAV) │
│  ├─ /api/v1/calendar  — Agenda (→ Nextcloud CalDAV)    │
│  ├─ /api/v1/contacts  — Contacts (→ Nextcloud CardDAV) │
│  ├─ /api/v1/meet      — Ultimeet (→ Jitsi JWT)        │
│  ├─ /api/v1/photos    — Ultiphotos (→ Immich API)     │
│  ├─ /api/v1/admin     — Administration                 │
│  ├─ /api/v1/search    — Recherche transversale         │
│  └─ /ws               — WebSocket realtime             │
├─────────────────────────────────────────────────────────┤
│  Services tiers (Docker)                                │
│  ├─ PostgreSQL 16     — DB partagée                    │
│  ├─ KeyDB             — Cache/sessions                  │
│  ├─ RustFS            — Object storage S3              │
│  ├─ Authentik         — Auth OIDC/SAML                 │
│  ├─ Nextcloud (nginx+FPM) — Drive/Cal/Contacts (/cloud) │
│  ├─ Jitsi             — Visioconférence                │
│  └─ Immich            — Photos/ML                      │
└─────────────────────────────────────────────────────────┘

Quick Start

# 1. Copy environment file
cp .env.example .env
# Edit secrets once at the top of .env (POSTGRES_PASSWORD, RUSTFS_SECRET_KEY, etc.)
# Defaults use changeme — must match Authentik blueprints (deploy/authentik/blueprints/).

# 2. Start stack (core + modules enabled by flags)
./deploy/compose-up.sh up -d --build

Auto-configured on first start:

  • SQL migrations (ULTID_AUTO_MIGRATE=true, embedded in ultid)
  • Authentik OAuth apps Ultimail (ulti-backend) and Nextcloud via blueprints in deploy/authentik/blueprints/
  • OIDC issuer for ultid via internal nginx: ULTID_OIDC_ISSUER=http://nginx/auth/application/o/ulti/

Frontend suite unifié (gmail-interface-clone — mail + drive + contacts) : .env.local avec NEXT_PUBLIC_APP_URL=http://localhost, puis pnpm devhttp://localhost/mail/ et http://localhost/drive/

Service URL
API / Auth http://localhost
Ultimail http://localhost/mail/
UltiDrive http://localhost/drive/
Grafana http://localhost:3002

Development

# Unit tests (fast, no Docker)
make test
# or: go test ./...

# Integration tests (Docker required — Postgres + MinIO via testcontainers)
cp .env.test.example .env.test   # optional overrides
make test-integration

# Run a single domain or test
make test-integration-mail
go test -tags=integration ./internal/integrationtest/mail/... -run TestMailSettingsCRUD -count=1 -v

# Enable optional suite modules (Nextcloud, Immich, Jitsi)
# ULTI_TEST_NEXTCLOUD=1 ULTI_TEST_NEXTCLOUD_URL=http://localhost:8081 make test-integration

Integration tests use a real PostgreSQL database (ephemeral container by default), miniredis, MinIO, and a test OIDC issuer that signs JWTs. Set ULTI_TEST_INTEGRATION=1 to run them. See .env.test.example for all variables.

# Run locally (needs PG, KeyDB, RustFS, Authentik running; loads .env with {{VAR}} expansion)
go run ./cmd/ultid

# Build
go build -o ultid ./cmd/ultid

# Migrations run automatically on ultid start. To disable: ULTID_AUTO_MIGRATE=false

# Manual migrate (optional)
go run ./cmd/envexpand -in .env -out .env.resolved
source <(grep -v '^#' .env.resolved | sed 's/^/export /')
migrate -path migrations -database "$ULTID_DB_URL" up

Reverse proxy (nginx)

Un seul nginx expose lentrée HTTP (:80) et route :

Chemin Service
/api/* ultid
/ws ultid (WebSocket)
/auth/* Authentik
/meet/* Jitsi (si JITSI_ENABLED=true)
/cloud/* Nextcloud nginx+FPM (si NEXTCLOUD_ENABLED=true)
/mail/*, /drive/*, /contacts, /agenda, /account, /settings, /admin/* Suite frontend (MAIL_FRONTEND_UPSTREAM, défaut host.docker.internal:3004 ; Docker : suite-frontend:3000)

Nextcloud : FPM + nginx dédié ; ultid appelle NEXTCLOUD_URL en interne (http://nextcloud:80).
Caddy retiré : un seul proxy évite la double couche ; TLS plus tard (certbot, Traefik, ou listen 443 nginx).

Centralized secrets

Set passwords and keys once in the Secrets section at the top of .env. Derived values reference them with {{POSTGRES_PASSWORD}}, {{RUSTFS_SECRET_KEY}}, etc. Expansion runs via:

  • ./deploy/compose-up.sh — writes .env.resolved for Docker Compose
  • go run ./cmd/envexpand -in .env -out .env.resolved — manual export for migrate/scripts
  • go run ./cmd/ultid — expands .env in-process before reading config

Runtime secret files are also supported with *_FILE variables (example: ULTID_OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret).

Mail credentials are encrypted at rest with AES-GCM using MAIL_CREDENTIAL_KEYS (key_id:base64key,...) and MAIL_ACTIVE_CREDENTIAL_KEY_ID. Secret rotation policy is enforced through:

  • SECRET_ROTATION_MAX_AGE
  • ULTID_OIDC_CLIENT_SECRET_ROTATED_AT
  • MAIL_CREDENTIAL_KEY_ROTATED_AT
  • MAIL_WEBHOOK_SHARED_SECRET_ROTATED_AT

Observability (Prometheus / Grafana)

ultid exposes Prometheus metrics at /metrics (see internal/observability/metrics.go). The core Docker Compose stack includes Prometheus and Grafana with configs under deploy/observability/:

File Purpose
deploy/observability/prometheus/prometheus.yml Scrape ultid + self; loads alert rules
deploy/observability/prometheus/alerts.yml Alert rules: IMAP sync stalled, outbox backlog, HTTP 5xx rate
deploy/observability/grafana/ultid-baseline.json Baseline dashboard (HTTP latency/errors, IMAP sync, outbox, webhooks)
deploy/observability/grafana/provisioning/ Grafana datasource + dashboard auto-load

Start with the rest of the stack (./deploy/compose-up.sh up -d), then open:

Service URL Notes
Prometheus http://localhost:9090 Targets: ultid, prometheus
Grafana http://localhost:3002 Login from .env (GRAFANA_ADMIN_USER / GRAFANA_ADMIN_PASSWORD, default admin / admin); dashboard Ultid Baseline under folder Ultid

Alertmanager — not included in compose; route labels service=ultid and severity (critical, warning) to your on-call channels when you add it.

Stack

Component Technology
Backend Go 1.25+ (chi, pgx, go-imap, go-smtp)
Database PostgreSQL 16
Cache KeyDB (Redis-compatible, multi-threaded)
Object Storage RustFS (S3-compatible, Apache 2.0)
Auth Authentik (OIDC/SAML)
Files/Cal/Contacts Nextcloud headless (WebDAV/CalDAV/CardDAV)
Video Jitsi Meet (JWT auth)
Photos Immich (ML classification)
Reverse Proxy nginx (TLS à ajouter via certbot ou autre)
Search PostgreSQL tsvector + GIN

Calendar API (mini doc)

Base path: /api/v1/calendar (module actif seulement si NEXTCLOUD_ENABLED=true).

Endpoints principaux

Endpoint Méthode Usage
/ GET Lister calendriers
/{calID}/events GET Lister événements d'un calendrier
/{calID}/events POST Créer événement
/events/* PUT Modifier événement avec If-Match
/events/* DELETE Supprimer événement
/events/response/* POST Répondre invitation participant
/freebusy POST Obtenir disponibilités (free/busy)
/events/meet-link/* POST Générer lien Meet et l'injecter dans l'événement

* représente un chemin CalDAV absolu, ex: /remote.php/dav/calendars/{user}/{calendar}/{uid}.ics

Event payload (create / update)

{
  "uid": "team-sync-2026-05-23",
  "summary": "Team sync",
  "description": "Weekly project update",
  "location": "Paris office",
  "start": "20260523T090000Z",
  "end": "20260523T100000Z",
  "organizer": "alice@example.com",
  "attendees": [
    { "email": "bob@example.com", "name": "Bob", "status": "NEEDS-ACTION" },
    { "email": "carol@example.com", "name": "Carol", "status": "ACCEPTED" }
  ]
}

Update avec ETag / If-Match

Requête:

PUT /api/v1/calendar/events/remote.php/dav/calendars/alice/work/team-sync-2026-05-23.ics
If-Match: "68031b8f4d18f"
Content-Type: application/json

Réponse succès:

{ "etag": "\"68031b8f5f901\"" }

Si ETag obsolète, API renvoie 412 avec code etag_mismatch.

Réponse invitation participant

POST /api/v1/calendar/events/response/remote.php/dav/calendars/alice/work/team-sync-2026-05-23.ics
{
  "email": "bob@example.com",
  "response": "accepted",
  "if_match": "\"68031b8f5f901\""
}

response autorise: accepted, declined, tentative.

Free/Busy

POST /api/v1/calendar/freebusy
{
  "start": "2026-05-23T08:00:00Z",
  "end": "2026-05-23T18:00:00Z",
  "attendees": ["bob@example.com", "carol@example.com"]
}

Réponse (exemple):

{
  "attendees": [
    {
      "email": "bob@example.com",
      "periods": [
        { "start": "20260523T090000Z", "end": "20260523T100000Z", "type": "BUSY" }
      ]
    }
  ]
}

Création lien Meet depuis event

POST /api/v1/calendar/events/meet-link/remote.php/dav/calendars/alice/work/team-sync-2026-05-23.ics
{
  "if_match": "\"68031b8f5f901\""
}

Réponse:

{
  "meet_url": "https://your-domain/meet/team-sync-2026-05-23?jwt=...",
  "etag": "\"68031b8f8b102\""
}

Si module Meet désactivé (JITSI_ENABLED=false), API renvoie 409 avec code meet_disabled.

Erreurs API Calendar

HTTP code Cas
400 invalid_request_body JSON invalide, champ manquant, email participant invalide, format date invalide
400 request_body_too_large Payload dépasse limite handler (256KB)
401 auth.* Token manquant/invalide
403 auth.forbidden Permission calendrier insuffisante (read/write)
404 attendee_not_found Email donné absent des ATTENDEE de l'événement
409 meet_disabled Endpoint meet-link appelé alors que Jitsi désactivé
412 etag_mismatch If-Match obsolète sur update event/response/meet-link
500 internal_error Erreur Nextcloud/CalDAV ou erreur interne backend

Format standard:

{
  "code": "etag_mismatch",
  "message": "etag does not match current resource version",
  "trace_id": "req_01HZ..."
}

Project Structure

├── cmd/ultid/          — Entry point
├── internal/
│   ├── api/            — HTTP handlers (mail, admin, drive, calendar, contacts, meet, photos)
│   ├── mail/           — IMAP sync, SMTP send, rules engine, webhooks
│   ├── nextcloud/      — WebDAV/CalDAV/CardDAV client
│   ├── meet/           — Jitsi JWT generation
│   ├── photos/         — Immich API client
│   ├── auth/           — OIDC verification
│   ├── search/         — Full-text search
│   ├── realtime/       — WebSocket hub
│   └── config/         — Environment config
├── migrations/         — SQL migrations
├── deploy/             — Docker Compose configs
│   ├── docker-compose.yml  — Core stack
│   ├── observability/      — Prometheus alerts + Grafana dashboard
│   ├── nginx/
│   ├── nextcloud/
│   ├── jitsi/
│   └── immich/
├── Dockerfile          — Multi-stage build
└── .env.example