Saltar al contenido principal

FlowPOS Architecture: Multi-Tenancy, Signup Automation & Tax Adapter Pattern

This document describes how FlowPOS implements multi-tenancy, automated tenant provisioning on signup, and the Tax Adapter pattern for global electronic invoicing scalability.


1. Multi-Tenancy Architecture​

Strategy: Row-Level Isolation with business_id​

FlowPOS uses shared database, shared schema with row-level isolation via a business_id column on every tenant-scoped table. This is the most common and maintainable approach for SaaS applications.

Key Points​

  • Single database instance for all tenants (dev, staging, production each have one DB)
  • No separate DB per client — avoids operational overhead
  • No schema-per-tenant — all businesses share the same schema
  • Tenant identifier: business_id (UUID) on all business-scoped tables

Database Schema​

All tenant-scoped tables include business_id:

user                    (global - no business_id)
business (tenant root - no business_id on self)
business_user (links user ↔ business)
document_counter (business_id)
product (business_id)
sale (business_id)
customer (business_id)
location (business_id)
address (business_id)
inventory_* (business_id)
credit_note (business_id)
debit_note (business_id)
fel_cancellation (business_id)
accounts_payable_* (business_id)
accounts_receivable_* (business_id)
... and 50+ more tables

Tables Without business_id​

  • user — global identity
  • country, currency — reference data
  • System tables: module, parameter_catalog, communication_template (with business_id: null for system-wide templates)
  • Join tables that derive context from parent (e.g., sale_detail via sale.business_id)

Application-Layer Enforcement​

  • Backend: Every repository/service filters by business_id from the authenticated user's context
  • Guards: RolesGuard and CASL policies enforce business_id via role_by_business_id in Firebase custom claims
  • Frontend: API calls include businessId from user context (selected business)

Row-Level Security (RLS) — Future Enhancement​

PostgreSQL RLS is not yet enabled in migrations. The Metabase docs (docs/metabase/) describe an RLS-based strategy for defense-in-depth:

  • Create one PostgreSQL role per business
  • Enable RLS on tenant tables
  • Use current_setting('app.current_business')::uuid in policies
  • Metabase would connect with per-tenant DB roles for embedded dashboards

Current state: Application-layer enforcement only. RLS can be added gradually for additional security without changing the application logic.


2. Signup & Tenant Provisioning Automation​

Flow: No Separate DB Per Client​

When a user signs up, the system does not spin up a new database instance. Instead, it:

  1. Creates a row in the user table
  2. Guides the user through onboarding
  3. Creates a business row and links it via business_user
  4. Updates Firebase custom claims with role_by_business_id

All of this happens within the existing shared database.

Signup Flow (Step by Step)​

  1. Firebase Auth: User signs up with email/password or Google (createUserWithEmailAndPassword, signInWithRedirect)
  2. Backend createUser or getOrCreateUser:
    • Creates user record with firebase_oauth_uid, email, full_name
    • Updates Firebase custom claims with db_user_id
  3. Frontend redirects to onboarding (e.g., /onboarding in PWA)
  4. Onboarding Orchestrator (OnboardingOrchestratorService):
    • Step 1 — Business: upsertOnboardingBusiness creates business, business_user (Owner), updates Firebase claims with role_by_business_id
    • Step 2 — Location: upsertUserLocation creates location, address, updates onboarding_status
    • Step 3 — Billing: patchOnboardingBill updates business with FEL credentials, legal name, tax ID; sets oboarding_completed: true in Firebase claims

Key Files​

ComponentPath
Onboarding orchestratorapps/backend/src/users/application/onboarding-orchestrator.service.ts
Users service (createUser)apps/backend/src/users/application/users.service.ts
Onboarding PWA pageapps/frontend-pwa/src/pages/OnboardingPage.tsx
Onboarding formsapps/frontend-pwa/src/components/onboarding/

Design Decisions​

  • Single DB: Easier ops, backups, migrations
  • Onboarding as a flow: User completes business, location, billing in sequence — no "empty" tenants
  • Firebase claims: role_by_business_id and db_user_id drive authorization; no extra session storage

3. Tax Adapter Pattern (Global Scalability)​

Purpose​

To support multiple countries' electronic invoicing (FEL, SAT, DIAN, TaxJar, etc.) without coupling business logic to a single certifier. The adapter pattern allows adding new regions by implementing an interface and registering the adapter.

Interface: ElectronicCertificationProvider​

// apps/backend/src/fel/domain/electronic-certification-provider.interface.ts

export interface ElectronicCertificationProvider {
certifyDocument({
taxId,
xmlContent,
apiUrl,
token,
}: IProviderCertifyDocumentParameters): Promise<undefined>;

getSharedInfo({
taxId,
data1,
data2,
username,
apiUrl,
token,
}: IProviderGetSharedInfoParameters): Promise<unknown>;
}

Current Implementations (Guatemala — FEL)​

AdapterFileCertifier
Digifactprovider-digifact.service.tsDigifact (Guatemala)
Infileprovider-infile.service.tsInfile (Guatemala)
RPAfelApiprovider-rpafelapi.service.tsRPAfelApi intermediary (Guatemala)

Routing​

  • ProviderService maintains a providerMap keyed by providerName
  • Document carries felProviderData.providerName (e.g., "digifact", "infile", "rpafelapi")
  • FelService.certifyDocument gets provider via this.providerService.getProvider(felProviderData.providerName)
  • RPAfelApi path is also controlled by env USE_RPA_FEL_API=true for the intermediary API

Strategy for Going Global​

Do not build adapters for Mexico, Colombia, USA, etc. yet. Build the interface and keep Guatemala as the beachhead. When traction comes in another country:

  1. Implement ElectronicCertificationProvider for that country (e.g., MexicoSATAdapter, ColombiaDIANAdapter, USATaxJarAdapter)
  2. Register it in ProviderService.providerMap
  3. Add country/provider selection in business settings and document flow
  4. Optionally hire a contractor for the module — the interface keeps it isolated

Document Model (Invoice / FelDocument)​

The Invoice interface in fel.interface.ts is Guatemala/FEL-oriented (e.g., establishmentCode, vatAffiliationCode, Spanish field names). For true global support:

  • Consider a canonical internal model + country-specific mappers (FEL, SAT, DIAN, etc.)
  • The ElectronicCertificationProvider could evolve to accept a canonical document; each adapter would map to the local format (XML, JSON, etc.)

4. Summary Table​

TopicImplementationLocation
Multi-tenancybusiness_id on all tenant tables, single DBMigrations: packages/backend/database/src/migrations/
RLSNot implemented; documented as future optiondocs/metabase/info.md
Signup automationOnboarding flow creates business, location, billing in shared DBOnboardingOrchestratorService
Tax adapter interfaceElectronicCertificationProviderfel/domain/electronic-certification-provider.interface.ts
Tax adapter implementationsDigifact, Infile, RPAfelApi (Guatemala)fel/infrastructure/provider-*.service.ts
Provider routingProviderService + providerMapfel/application/provider.service.ts

5. Apps Overview (Backend & Frontend PWA)​

Backend (apps/backend)​

  • Framework: NestJS 11
  • Structure: Domain-driven, one folder per bounded context (e.g., sales/, fel/, businesses/)
  • Layers: interfaces/ (controllers, DTOs), application/ (services), domain/ (interfaces, types), infrastructure/ (repositories)
  • Notable modules: FEL, sales, businesses, onboarding (users), cash-register, accounts-payable, accounts-receivable, communications, metabase
  • Auth: Firebase Admin; RolesGuard + CASL for business_id-scoped authorization
  • Database: Kysely ORM, migrations in packages/backend/database

Frontend PWA (apps/frontend-pwa)​

  • Stack: React, Vite, PWA
  • Structure: pages/, components/, services/, hooks/, contexts/, i18n/
  • Auth: Firebase Auth (email/password, Google); token in requests
  • Onboarding: OnboardingPage with step-based flow (business → location → billing)
  • Services: One service per backend domain (e.g., salesService, felService, onboardingService)

  • CURSOR.md — Project context and patterns
  • docs/metabase/info.md — RLS strategy for Metabase
  • docs/fel/ — FEL architecture, RPAfelApi integration, field mappings