Architecture

Storno.ro is a multi-repo platform with several applications and supporting infrastructure services. This page describes how the components fit together.


Service Topology

                    ┌──────────────────────────────┐
                    │       Nginx (reverse proxy)   │
                    │  app.storno.ro  api.storno.ro │
                    └──────┬───────────────┬────────┘
                           │               │
                    ┌──────▼──────┐  ┌─────▼──────┐
                    │  Frontend   │  │  Backend    │
                    │  Nuxt 4     │  │ Symfony 7.4 │
                    │  :3000      │  │  :8000      │
                    └──────┬──────┘  └──┬──┬──┬────┘
                           │            │  │  │
              ┌────────────┘     ┌──────┘  │  └──────┐
              │                  │         │         │
        ┌─────▼─────┐    ┌──────▼──┐  ┌───▼───┐  ┌──▼────────┐
        │  Mobile    │    │ MySQL   │  │ Redis │  │ Centrifugo │
        │ React      │    │ 8.0     │  │ 7     │  │ v5         │
        │ Native     │    └─────────┘  └───────┘  └────────────┘
        └────────────┘
ServiceRepositoryTechnologyRole
Backendstornoro/stornoPHP 8.2 + Symfony 7.4 + NginxREST API, business logic, async workers
Frontendstornoro/stornoNuxt 4 (Vue 3, SSR)Web application with server-side rendering
Mobilestornoro/storno-mobile-appReact Native + ExpoiOS and Android mobile app
Docsstornoro/docsNext.js + MarkdocAPI documentation
CLIstornoro/storno-cliTypeScript (MCP)CLI tool for AI assistants
MySQL8.0Primary database (57 entities)
Redis7.xCache, message queue, rate limiting, locks
Centrifugov5WebSocket server for real-time updates

The backend container bundles PHP-FPM, Nginx, Supervisor (worker management), and a Java 17 runtime for UBL XML validation and digital signature verification.


Backend Structure

The Symfony backend is organized into domain-oriented controllers, entities, and services:

LayerCountExamples
Controllers68Invoices, Auth, E-Invoice, Webhooks, Admin
Entities57Invoice, Client, Payment, Company, User
Services95PDF generation, e-Factura sync, email, import
Message handlers17Async PDF, ANAF submission, webhook dispatch
Console commands21Cron jobs, maintenance, data migration

Key Service Domains

  • Anaf/ (11 services) — UBL XML generation, ANAF SPV communication, XML parsing, validation
  • Import/ (22 services) — Data migration from 12 invoicing systems (SmartBill, Ciel, eMag, etc.)
  • Storage/ (5 services) — Multi-backend file storage with encryption (S3, local)
  • Export/ (3 services) — CSV, Saga XML, ZIP archive exports
  • Backup/ (3 services) — Company-level backup and restore

Data Flow

Invoice Lifecycle

Draft → Issued → Sent to ANAF → Validated/Rejected
  │        │          │                │
  │        │          │                └─ Centrifugo → real-time UI update
  │        │          └─ Async: SubmitToAnafMessage (Redis queue)
  │        └─ Async: GeneratePdfMessage (Redis queue)
  └─ Sync: Validate + persist to MySQL
  1. Create draft — User creates invoice via API. Data validated and persisted to MySQL.
  2. Issue — Generates UBL 2.1 XML, stores to S3, dispatches async PDF generation.
  3. Submit to provider — Async worker uploads XML to e-invoice provider. Status set to sent_to_provider.
  4. Provider sync — Cron job polls provider for status updates. Invoice marked validated or rejected.
  5. Real-time update — Each status change publishes to Centrifugo. Connected clients update instantly.

e-Factura Sync

A cron job runs every 15 minutes to synchronize with ANAF:

Cron → EFacturaSyncService
  ├─ Check outgoing invoice statuses (validated / rejected)
  ├─ Download incoming invoices (auto-create Invoice + Client + Products)
  ├─ Create EFacturaMessage audit records
  └─ Publish real-time notifications via Centrifugo

Incoming invoices from suppliers are automatically parsed from UBL XML into full Invoice entities with line items.

PDF Generation

PDF generation uses a dual strategy for reliability:

  1. Primary: HTTP request to Java service (~100ms) — UBL XML → PDF
  2. Fallback: wkhtmltopdf shell command (~2–4s) — HTML template → PDF

Both paths store the resulting PDF in S3 via Flysystem.


Real-Time Updates

Centrifugo provides WebSocket-based real-time updates to all connected clients.

Channel Structure

ChannelPurposeFeatures
invoices:{company}Invoice status changesHistory (20 messages, 2 min TTL)
notifications:{user}User notificationsPresence, history (50 messages, 1 hour TTL), recovery
dashboard:{company}Dashboard statsPresence tracking
user:{user}User-specific eventsHistory (10 messages, 1 min TTL)

Publishing Flow

The backend queues messages during request processing and flushes them at the end of the request lifecycle:

Controller / Message Handler

CentrifugoService.queue(channel, data)    ← buffer messages

CentrifugoFlushSubscriber (kernel.terminate / console.terminate)

CentrifugoService.flush()                 ← batch HTTP POST to Centrifugo

Centrifugo broadcasts to subscribed clients

This batching approach minimizes HTTP calls to Centrifugo — a single request can carry updates for multiple channels.


Async Job Processing

Background jobs are processed via Symfony Messenger with Redis as the transport:

MessagePurpose
SubmitToAnafMessageUpload invoice XML to ANAF
CheckAnafStatusMessagePoll ANAF for submission status
SyncCompanyMessageFull e-Factura sync for a company
GeneratePdfMessageGenerate invoice PDF
GenerateZipExportMessageCreate ZIP archive of invoices
SendExternalNotificationMessageSend push notifications
SendPushNotificationMessageFirebase push to mobile devices
ProcessImportMessageImport data from external systems
DispatchWebhookMessageDeliver webhook payloads
DeleteUserAccountMessageGDPR account deletion
DeleteCompanyDataMessageCompany data removal
ResetCompanyDataMessageCompany data reset
SendInvitationEmailMessageOrganization invitations
SendEmailConfirmationMessageEmail verification

Supervisor runs the Messenger worker process, which continuously polls the Redis queue and processes messages.


Multi-Tenancy

Organization (root tenant)
├─ User[] (members with roles)
└─ Company[] (business entities)
   ├─ Invoice[], ProformaInvoice[], DeliveryNote[]
   ├─ Client[], Supplier[], Product[]
   ├─ Payment[], BankAccount[]
   ├─ DocumentSeries[], VatRate[]
   ├─ WebhookEndpoint[]
   ├─ EmailTemplate[]
   └─ AnafToken (e-Factura credentials)
  • Organization is the billing and membership boundary
  • Company is the data isolation boundary — all API requests require an X-Company header
  • Users can belong to one organization with access to multiple companies
  • RBAC with 5 roles (Owner, Admin, Accountant, Member, Viewer) and 40+ granular permissions

See Multi-Tenancy for details.


Authentication

Multiple authentication methods feed into a unified JWT token flow:

Email/Password ──┐
Google OAuth ────┤──→ [MFA Challenge?] ──→ JWT Token ──→ API Access
Passkeys ────────┤         │
API Keys ────────┘         └─ TOTP or Backup Code
  • Email/password and Google OAuth trigger MFA if enabled
  • Passkeys skip MFA (inherently multi-factor)
  • API keys bypass MFA (scoped programmatic access)
  • JWT tokens are RSA-signed (RS256) with 1-hour expiry and 30-day refresh tokens

See Authentication for details.


Deployment

Five containers managed via Docker Compose:

docker compose up -d    # backend, frontend, db, redis, centrifugo

Kubernetes

Helm chart available with configurable resource limits, persistent volumes, and ingress rules. See System Requirements for resource allocation.

CI/CD

Each repository has its own GitHub Actions workflow that builds Docker images and deploys via SSH with a blue-green strategy:

  1. Build and push images to GHCR
  2. Start new containers on alternate ports
  3. Run health checks
  4. Swap traffic if healthy, rollback if not

Backend and frontend are deployed from the monorepo (stornoro/storno). Docs and other services deploy independently from their own repositories.