Tech stack
Audience
PLANA staff, technical partners. The choices below are intentional and mostly long-standing; this page captures the why alongside the what.
PLANA's stack is FOSS-first and deliberately narrow. We pick one tool per job and stick with it across products. The rule is "fewer, deeper" rather than "one-tool-per-team".
Cloud and platform
| Layer | Choice | Why |
|---|---|---|
| Cloud | Exoscale, zone bg-sof-1 (Sofia, BG) | EU data residency. Bulgarian provider. No US-cloud reliance. |
| Compute | Exoscale instances + SKS managed Kubernetes | Managed K8s without the operational tax of running etcd ourselves |
| Object storage | Exoscale Simple Object Storage (S3-compatible) | Same vendor, S3 tooling |
| Block storage | Exoscale block storage, retain reclaim policy | Per-tool DBs survive PVC accidents |
| Load balancer | Exoscale NLB | TCP-level; TLS terminates inside the cluster |
| DNS | Exoscale DNS | API-controllable; cert-manager DNS-01 works |
| Email (outbound) | Mailu on a separate Hetzner box | Email reputation is non-trivial; cluster-cluster mail is bad practice |
Containers and orchestration
| Layer | Choice |
|---|---|
| Kubernetes | Exoscale SKS, currently v1.35.3 |
| CNI | Calico, with NetworkPolicy enforcement on |
| Ingress | Envoy Gateway v1.7.1 (Kubernetes Gateway API) |
| Provisioning | Crossplane composite resources (custom XRDs) |
| GitOps | Flux v2 |
| Image registry | Forgejo registry at git.planapulse.com |
| Container runtime | containerd (Exoscale default) |
| TLS | cert-manager + Let's Encrypt, DNS-01 challenge |
Languages and frameworks
Backend — Node.js
| Choice | Why |
|---|---|
| JavaScript ESM, not TypeScript | Reduce build complexity; TS adds CI time, types we already test against runtime |
| Fastify v5 (not Koa, not Express) | Best perf, schema-first, modern plugin model |
| Ramda | Functional utilities; consistent across services |
| BullMQ | Redis-backed queues; widely-deployed pattern |
Backend — Python
| Choice | Why |
|---|---|
| Python 3.12 | Most recent stable at the time the AI services started |
| FastAPI | Async, schema-first, OpenAPI generation built in |
| asyncpg | Native async PostgreSQL driver, very fast |
| PyJWT | Standard JWT library |
| Anthropic SDK (claude-opus-4-7 / claude-sonnet-4-6) | Primary LLM provider |
| OpenRouter | Fallback router for non-Anthropic models |
Backend — Odoo (Python, but separate world)
| Choice | Why |
|---|---|
| Odoo Community 18.0 | Current default for new tenants |
| OCA modules | Where the community already solved it, we use their solution |
PLANA plana_* modules | Thin layer over OCA + Bulgarian fiscal pack |
| PostgreSQL | Odoo's only supported DB |
Frontend
| Choice | Why |
|---|---|
| Vue 3.5 + Nuxt 3 | The Vue ecosystem's "batteries included" framework, used for pulse-account, pulse-admin, pulse-portal |
| Vue 3 + vite-ssg (static) | pulse-website — full static build, served by nginx, fast |
| Vue 3 + Vite SPA (no router) | bos-portal — single-page app, in-memory view switching |
| VitePress 1.6 | This documentation portal |
| Pinia | State management |
| JavaScript ESM | Same rule as backend; no TypeScript |
| Ramda | Shared utility library |
Mobile
| Choice | Why |
|---|---|
| Ionic Vue + Capacitor 7 | Reuse the Vue frontend stack on iOS and Android |
| Plan, not yet built | First mobile artifacts targeted for 2026-Q3 |
Data and messaging
| Layer | Choice |
|---|---|
| Relational DB | PostgreSQL (single VM at pg01) |
| Connection pooling | pgbouncer, transaction mode |
| Cache + sessions + queue | Redis (specifically Valkey 7), single instance |
| Event bus | Redis Streams (PLANA:events), CloudEvents 1.0 |
| Search (in-app) | PostgreSQL trigram + Odoo's own ORM search |
| Vector store | pgvector inside pulse-data (768-dim sentence-transformer embeddings) |
| File store | NFS export at nfs01.planapulse.com |
| Backups | Exoscale SOS (S3-compatible) |
| Future scale | Kafka only if PLANA:events exceeds ~1M events/day |
Auth and secrets
| Layer | Choice |
|---|---|
| Staff SSO | Authentik Community (self-hosted) |
| Tenant SSO | plana_auth Odoo module against Authentik OIDC |
| Service-to-service | X-API-Key header (per-service) |
| User-facing tokens | JWT HS256, 15-min access + 30-day refresh, pa_token cookie |
| Secrets at rest | SOPS age encrypted YAML in infra/secrets/ |
| Secrets mirror | Vaultwarden (UI-friendly secondary copy) |
| 2FA | Mandatory TOTP for staff, optional for tenant users |
We deliberately do not use HashiCorp Vault. SOPS + Vaultwarden gives us the same security with one-tenth the operational complexity. The OpenBao POC explored a deeper secrets-management story and was parked.
Observability
| Layer | Choice |
|---|---|
| Metrics | Prometheus + Grafana |
| Logs | Loki (operator-grade), 90-day retention |
| Synthetic probes | Blackbox Exporter |
| Alerts | Alertmanager → Matrix room only (no email pages) |
| Tracing | None currently; OTLP via Prometheus when needed |
| Audit | In-cluster ValidatingAdmissionWebhook → Loki |
CI/CD
| Layer | Choice |
|---|---|
| Git host | Forgejo at git.planapulse.com (not GitHub or GitLab) |
| CI runners | Forgejo Actions runners in the cluster |
| Build images | Kaniko (rootless container builds) |
| Workflow templates | Shared in ci-templates repo |
| Deploy | Mostly Flux GitOps; a few legacy kubectl set image jobs |
LLM and AI
| Layer | Choice |
|---|---|
| Primary LLM | Anthropic Claude (claude-opus-4-7 for high quality; claude-sonnet-4-6 for routine workloads; claude-haiku-4-5 for low-cost tasks) |
| Fallback | OpenRouter for non-Anthropic models when warranted |
| Embeddings | sentence-transformers (768 dims) inside pulse-data |
| Agent framework | In-house (ai-agents repo, FastAPI + Anthropic SDK + tool registry) |
| Streaming | SSE end-to-end, native to Anthropic SDK |
| Shadow Mode | All agents run with shadow_mode=True by default; writes simulated and logged for review |
What we do NOT use
This list is short and intentional:
- TypeScript — ESM + JSDoc is enough; the build cost isn't worth it
- GraphQL — REST + Fastify schema gives us the same DX with less infra
- Kafka — Redis Streams is plenty until we cross ~1M events/day
- HashiCorp Vault — SOPS + Vaultwarden is simpler and adequate
- Falco / runtime security agents — dropped in May 2026 in the simplification sweep; the audit-webhook + Loki gives us what we need
- Bitnami / vendor-rebranded images — we use official upstream images or build our own (see FOSS first)
- OpenTelemetry collector — Prometheus + Loki is enough; we will add OTLP later only if traceability gaps motivate it
Where to read more
- Policies → FOSS first — the rule that drives several of the "no vendor X" choices
- Services — per-service detail on which slice of this stack each one uses