Maxx ERP Blueprint
This document defines the full equivalent of what AncoraERP does for maxxcomputers, rebuilt lean in SvelteKit + PocketBase + TypeScript. Nothing gets lost. Everything gets better.
1. System Overview
Section titled “1. System Overview”graph TD
subgraph Frontend["SvelteKit — Cloudflare Pages"]
UI["~20 routes\n(SSR + client navigation)"]
end
subgraph Backend["PocketBase — self-hosted / Fly.io"]
COLL["Collections + REST + Realtime"]
HOOKS["JS Hooks\n(onRecordBefore/AfterCreate)\n— all business logic lives here"]
SCHED["Scheduled jobs\n(BNR rates, supplier feeds)"]
FILES["File storage\n(invoice PDFs, attachments)"]
end
subgraph External["External APIs"]
BNR["bnr.ro XML\ncurrency rates"]
ANAF["webservicesp.anaf.ro\nCUI lookup + e-Factura SPV"]
SUPPLIERS["20+ IT distributor\nprice feed APIs"]
SMTP["Resend\n(transactional email)"]
COURIER["Innight courier\nXML shipment API"]
end
UI <-->|PocketBase SDK| COLL
HOOKS -->|auto-triggered on write| COLL
SCHED -->|cron| BNR
SCHED -->|cron| SUPPLIERS
UI -->|server-side fetch| ANAF
HOOKS -->|on invoice approve| SMTP
UI -->|shipment create| COURIER
2. Data Model (PocketBase Collections)
Section titled “2. Data Model (PocketBase Collections)”Core Collections
Section titled “Core Collections”erDiagram
companies {
string id PK
string name
string cui "Romanian fiscal code"
string reg_com "Trade register number"
string address
string iban
string bank
int county_id FK
string logo "file"
string invoice_series "e.g. MX"
int next_invoice_number "auto-incremented in hook"
}
users {
string id PK
string company FK
string email
string name
string role "admin|operator|viewer"
string default_warehouse FK
}
products {
string id PK
string company FK
string cod "internal code"
string name
string unit "BUC|KG|L|M|SET"
float vat_rate "19|9|5|0"
float list_price "sell price lei"
float cost_price "last purchase price"
float stock "computed — updated by hooks"
string category FK
string barcode
bool track_serial "true for laptops, phones, etc."
bool active
}
product_supplier_codes {
string id PK
string product FK
string supplier FK "tert"
string supplier_code "supplier's own SKU"
float supplier_price "latest from price feed"
date price_updated_at
}
terti {
string id PK
string company FK
string name
string cui "fiscal code — validated via ANAF on create"
string reg_com
string type "C|F|CF (client|supplier|both)"
string address
string county FK
string phone
string email
string iban
string bank
bool vat_payer
bool active
}
warehouses {
string id PK
string company FK
string name "DEPOZIT|MAGAZIN|SERVICE"
string type "storage|retail|service"
}
categories {
string id PK
string company FK
string name "Laptops|Telefoane|Periferice|Software|Servicii"
string parent FK "self-reference for tree"
}
currencies {
string id PK
string company FK
date date
float eur
float usd
float gbp
float chf
string source "BNR|manual"
}
Transaction Collections
Section titled “Transaction Collections”erDiagram
invoices_out {
string id PK
string company FK
string number "MX-2026-0001 — generated in hook"
date date
string client FK "terti"
string status "draft|confirmed|cancelled"
string currency "RON|EUR|USD"
float exchange_rate "from currencies on date"
float total_net
float total_vat
float total
string payment_type "cash|bank_transfer|card|check"
date due_date
bool efactura_sent
string efactura_id "ANAF upload ID"
string efactura_status "pending|accepted|rejected"
string pdf "file — generated on confirm"
string notes
}
invoice_lines_out {
string id PK
string invoice FK "invoices_out"
string product FK
string product_name "snapshot at time of invoice"
float qty
string unit
float price "unit price excl VAT"
float vat_rate
float discount_pct
float line_total "computed: qty * price * (1-disc)"
float line_vat "computed: line_total * vat_rate"
string serial_numbers "JSON array — if track_serial"
}
invoices_in {
string id PK
string company FK
string supplier FK "terti"
string supplier_invoice_number
date date
date due_date
string status "draft|received|paid"
float total_net
float total_vat
float total
string currency
float exchange_rate
string notes
string attachment "file — scanned invoice"
}
invoice_lines_in {
string id PK
string invoice FK "invoices_in"
string product FK
string product_name "snapshot"
float qty
float price
float vat_rate
float line_total
string serial_numbers "JSON array"
}
orders_out {
string id PK
string company FK
string number "CMD-2026-0001"
date date
string client FK "terti"
string status "draft|confirmed|fulfilled|cancelled"
string agent FK "users"
float total_net
float total_vat
string notes
}
order_lines_out {
string id PK
string order FK "orders_out"
string product FK
float qty
float price
float vat_rate
float qty_delivered "updated as invoices are created"
}
orders_in {
string id PK
string company FK
string number "PO-2026-0001"
date date
string supplier FK "terti"
string status "draft|sent|received|cancelled"
float total_net
string notes
}
order_lines_in {
string id PK
string order FK "orders_in"
string product FK
float qty
float price
float qty_received "updated as supplier invoices arrive"
}
stock_movements {
string id PK
string company FK
string product FK
string warehouse FK
float qty_delta "positive = in, negative = out"
float unit_value "cost at time of movement"
string doc_type "invoice_out|invoice_in|transfer|adjustment"
string doc_id "FK to source document"
date date
string serial_number "if track_serial — one record per unit"
}
serial_numbers {
string id PK
string company FK
string product FK
string sn "the serial number"
string status "in_stock|sold|returned|in_service"
string client FK "set when sold"
date sold_date
date warranty_expires "sold_date + warranty months from product"
string invoice_out FK "which invoice it was sold on"
string invoice_in FK "which invoice it was received on"
}
warranties {
string id PK
string company FK
string serial_number FK
string client FK
date received_date
string problem_description
string status "open|in_service|returned_ok|returned_replaced|rejected"
date closed_date
string resolution
string return_invoice FK "invoices_out — replacement/repaired"
}
payments {
string id PK
string company FK
string tert FK "terti"
string direction "in|out"
float amount
string currency
float exchange_rate
string type "cash|bank_transfer|card|check|promissory"
date date
string reference "check number / bank ref / receipt number"
string notes
}
payment_invoice_links {
string id PK
string payment FK
string invoice FK "invoices_out OR invoices_in"
string invoice_type "out|in"
float amount_applied
}
price_lists {
string id PK
string company FK
string name "Retail|Wholesale|Partner"
float discount_pct "applied on top of list_price"
string client_category FK
}
supplier_offers {
string id PK
string company FK
string supplier FK "terti"
string product_code FK "product_supplier_codes"
float price
int stock_qty "supplier's reported stock"
date valid_date
string currency
}
licenses {
string id PK
string company FK
string product FK "software product"
string client FK "terti"
string license_key
date issue_date
date expiry_date "null = perpetual"
string invoice_out FK
string status "active|expired|revoked"
string notes
}
shipments {
string id PK
string company FK
string invoice_out FK
string courier "innight|cargus|sameday"
string awb
string status "created|picked_up|in_transit|delivered|returned"
date created_date
date delivered_date
string recipient_name
string recipient_address
float weight_kg
float declared_value
}
3. Business Logic — PocketBase Hooks
Section titled “3. Business Logic — PocketBase Hooks”All business logic lives in pb_hooks/. No logic in the frontend. The frontend is purely display + forms.
pb_hooks/├── invoices_out.pb.js — numbering, stock deduction, PDF trigger, e-Factura├── invoices_in.pb.js — stock increase, cost price update├── orders_out.pb.js — numbering, stock reservation check├── orders_in.pb.js — numbering├── payments.pb.js — mark invoices as paid, cash register balance├── serial_numbers.pb.js — warranty expiry computation├── warranties.pb.js — status notifications├── products.pb.js — low stock alert├── cron_bnr.pb.js — daily BNR rate fetch at 23:00└── cron_suppliers.pb.js — nightly supplier price feed importsHook: invoices_out.pb.js
Section titled “Hook: invoices_out.pb.js”// BEFORE CREATE — generate document number atomicallyonRecordBeforeCreateRequest((e) => { const company = $app.dao().findRecordById('companies', e.record.get('company')); const next = company.getInt('next_invoice_number') + 1; const series = company.getString('invoice_series'); const year = new Date().getFullYear(); const num = String(next).padStart(4, '0'); e.record.set('number', `${series}-${year}-${num}`); e.record.set('status', 'draft'); // save incremented counter — serialized, no race condition possible in SQLite company.set('next_invoice_number', next); $app.dao().saveRecord(company);}, 'invoices_out');
// AFTER CREATE status change to "confirmed" — deduct stock + update serial statusonRecordAfterUpdateRequest((e) => { if (e.record.get('status') !== 'confirmed') return; if (e.record.getOriginal().get('status') === 'confirmed') return; // already processed
const lines = $app .dao() .findRecordsByFilter('invoice_lines_out', 'invoice = {:id}', { id: e.record.id }, -1, 0, []);
for (const line of lines) { const productId = line.get('product'); const qty = line.getFloat('qty'); const warehouse = e.record.get('warehouse'); // default warehouse from company settings
// 1. Deduct product.stock const product = $app.dao().findRecordById('products', productId); const newStock = product.getFloat('stock') - qty; if (newStock < 0) throw new Error(`Insufficient stock for ${product.getString('name')}`); product.set('stock', newStock); $app.dao().saveRecord(product);
// 2. Create stock movement audit record const movement = new Record($app.dao().findCollectionByNameOrId('stock_movements')); movement.set('company', e.record.get('company')); movement.set('product', productId); movement.set('warehouse', warehouse); movement.set('qty_delta', -qty); movement.set('unit_value', line.getFloat('price')); movement.set('doc_type', 'invoice_out'); movement.set('doc_id', e.record.id); movement.set('date', e.record.get('date')); $app.dao().saveRecord(movement);
// 3. Mark serial numbers as sold (if tracked) const sns = JSON.parse(line.getString('serial_numbers') || '[]'); for (const sn of sns) { const snRecord = $app .dao() .findFirstRecordByFilter('serial_numbers', 'sn = {:sn} && company = {:c}', { sn, c: e.record.get('company') }); snRecord.set('status', 'sold'); snRecord.set('client', e.record.get('client')); snRecord.set('sold_date', e.record.get('date')); snRecord.set('invoice_out', e.record.id); // set warranty expiry const product2 = $app.dao().findRecordById('products', snRecord.get('product')); const warrantyMonths = product2.getInt('warranty_months') || 24; const soldDate = new Date(e.record.get('date')); soldDate.setMonth(soldDate.getMonth() + warrantyMonths); snRecord.set('warranty_expires', soldDate.toISOString().split('T')[0]); $app.dao().saveRecord(snRecord); } }
// 4. Queue PDF generation + e-Factura (async via job queue or direct call) $app.dao().runInTransaction(() => { schedulePdfGeneration(e.record.id); if (e.record.get('efactura_required')) { scheduleEFacturaSubmit(e.record.id); } });}, 'invoices_out');
// AFTER CREATE status change to "cancelled" — reverse stockonRecordAfterUpdateRequest((e) => { if (e.record.get('status') !== 'cancelled') return; if (e.record.getOriginal().get('status') !== 'confirmed') return; // ... reverse of the above: add stock back, set SNs to in_stock}, 'invoices_out');Hook: invoices_in.pb.js
Section titled “Hook: invoices_in.pb.js”// When supplier invoice is confirmed → increase stock + update cost priceonRecordAfterUpdateRequest((e) => { if (e.record.get('status') !== 'received') return; const lines = $app .dao() .findRecordsByFilter('invoice_lines_in', 'invoice = {:id}', { id: e.record.id }, -1, 0, []);
for (const line of lines) { const product = $app.dao().findRecordById('products', line.get('product')); const incomingQty = line.getFloat('qty'); const incomingPrice = line.getFloat('price');
// weighted average cost price const currentStock = product.getFloat('stock'); const currentCost = product.getFloat('cost_price'); const newCost = currentStock === 0 ? incomingPrice : (currentStock * currentCost + incomingQty * incomingPrice) / (currentStock + incomingQty);
product.set('stock', currentStock + incomingQty); product.set('cost_price', newCost); $app.dao().saveRecord(product);
// create stock movement // register serial numbers as in_stock if tracked }}, 'invoices_in');Hook: cron_bnr.pb.js
Section titled “Hook: cron_bnr.pb.js”// runs every day at 23:00cronAdd('bnr_rates', '0 23 * * *', () => { const response = $http.send({ url: 'https://www.bnr.ro/nbrfxrates.xml', method: 'GET' }); const xml = response.raw; // parse XML — PocketBase JS has no DOMParser but can regex/split BNR's simple format const date = xml.match(/PublishingDate>([^<]+)/)[1]; const rates = { eur: 0, usd: 0, gbp: 0, chf: 0 }; for (const [currency, field] of [ ['EUR', 'eur'], ['USD', 'usd'], ['GBP', 'gbp'], ['CHF', 'chf'] ]) { const match = xml.match(new RegExp(`currency="${currency}"[^>]*>([0-9.]+)`)); if (match) rates[field] = parseFloat(match[1]); } // upsert one record per company const companies = $app.dao().findRecordsByFilter('companies', '1=1', {}, -1, 0, []); for (const company of companies) { // findOrCreate currency record for this company+date }});4. Route Map (SvelteKit)
Section titled “4. Route Map (SvelteKit)”src/routes/├── (auth)/│ ├── login/+page.svelte ← company CUI + username + password│ └── logout/+server.ts│├── (app)/ ← all protected, layout checks session│ ├── +layout.svelte ← sidebar nav, company name, user menu│ ││ ├── dashboard/+page.svelte ← KPI cards + alerts (see §5)│ ││ ├── products/│ │ ├── +page.svelte ← searchable list, filter by category│ │ ├── [id]/+page.svelte ← detail: info + stock by warehouse + movement history + SNs + supplier offers│ │ └── new/+page.svelte│ ││ ├── clients/│ │ ├── +page.svelte ← list with outstanding balance column│ │ └── [id]/+page.svelte ← detail: invoices + payments + orders│ ││ ├── suppliers/│ │ ├── +page.svelte│ │ └── [id]/+page.svelte ← detail: purchase history + price offers│ ││ ├── sales/│ │ ├── invoices/│ │ │ ├── +page.svelte ← list: filter by status/client/date│ │ │ ├── new/+page.svelte ← invoice editor (see §6)│ │ │ └── [id]/+page.svelte ← view + PDF + e-Factura status + payment status│ │ ├── orders/│ │ │ ├── +page.svelte│ │ │ └── [id]/+page.svelte ← view + fulfillment status + convert to invoice│ │ └── proforma/ ← same as invoice but status=draft, no stock effect│ ││ ├── purchasing/│ │ ├── invoices/ ← receive goods, update stock│ │ │ ├── +page.svelte│ │ │ └── [id]/+page.svelte│ │ ├── orders/ ← send POs to suppliers│ │ │ ├── +page.svelte│ │ │ └── [id]/+page.svelte│ │ └── supplier-offers/+page.svelte ← compare prices across distributors│ ││ ├── stock/│ │ ├── +page.svelte ← current stock all products (filterable by warehouse)│ │ ├── movements/+page.svelte ← full audit log│ │ ├── serial-numbers/+page.svelte ← search SN, see history│ │ └── transfers/+page.svelte ← move stock between warehouses│ ││ ├── treasury/│ │ ├── +page.svelte ← cash balance + bank balance│ │ ├── payments/new/+page.svelte ← record payment (in or out), link to invoice(s)│ │ └── history/+page.svelte ← payment ledger│ ││ ├── warranty/│ │ ├── +page.svelte ← list open service tickets│ │ └── [id]/+page.svelte ← ticket detail, status updates│ ││ ├── licenses/+page.svelte ← active/expired software licenses sold to clients│ ││ ├── reports/│ │ ├── outstanding/+page.svelte ← AR + AP aging (who owes what)│ │ ├── stock-value/+page.svelte ← total inventory value by category│ │ ├── sales/+page.svelte ← revenue by period / by product / by client│ │ ├── margin/+page.svelte ← gross margin per product / invoice│ │ └── top-products/+page.svelte ← best sellers by qty and by revenue│ ││ └── settings/│ ├── company/+page.svelte ← name, CUI, IBAN, invoice series, logo│ ├── users/+page.svelte ← manage users + roles│ ├── warehouses/+page.svelte│ ├── categories/+page.svelte│ └── currencies/+page.svelte ← manual override + BNR sync trigger5. Dashboard
Section titled “5. Dashboard”graph TD
subgraph DASH["Dashboard — loads on every login"]
KPI1["💰 Today's sales\n(sum invoices_out today)"]
KPI2["📦 Low stock alerts\n(products.stock < min_stock)"]
KPI3["⚠️ Outstanding receivables\n(unpaid invoices_out > 30 days)"]
KPI4["📋 Open orders\n(orders_out status=confirmed)"]
KPI5["🔧 Open warranties\n(warranties status=open)"]
KPI6["📅 Licenses expiring\n(< 30 days)"]
FEED["Activity feed\n(last 20 documents)"]
end
All KPI cards are single PocketBase queries. No complex aggregation needed.
6. Invoice Editor — Most Complex Screen
Section titled “6. Invoice Editor — Most Complex Screen”sequenceDiagram
participant U as User
participant SK as SvelteKit
participant PB as PocketBase
participant ANAF as ANAF API
U->>SK: Open /sales/invoices/new
SK->>PB: GET /api/collections/terti?filter=type~'C'
PB-->>SK: client list
SK-->>U: render blank form
U->>SK: type CUI in client field
SK->>ANAF: GET /api/anaf/cui?cui=RO12345678
ANAF-->>SK: {name, address, ...}
SK-->>U: auto-fill client fields (or create new tert)
U->>SK: add product line (type product name/code)
SK->>PB: GET /api/collections/products?filter=name~'thinkpad'
PB-->>SK: matching products with stock + list_price
SK-->>U: show dropdown with stock indicator
U->>SK: set qty, adjust price, apply discount
SK-->>U: compute line_total + line_vat live (client-side)
SK-->>U: show total_net, total_vat, total (grouped by VAT rate)
U->>SK: POST confirm invoice
SK->>PB: PATCH /api/collections/invoices_out/:id {status: "confirmed"}
PB->>PB: hook fires — check stock, deduct, generate number
PB-->>SK: {id, number: "MX-2026-0042", ...}
SK->>PB: trigger PDF generation
SK-->>U: redirect to /sales/invoices/MX-2026-0042
U->>SK: click "Send e-Factura"
SK->>ANAF: POST SPV OAuth2 + UBL XML
ANAF-->>SK: upload_id
SK->>PB: PATCH efactura_id + status=pending
Invoice Line Item component behaviour:
Section titled “Invoice Line Item component behaviour:”- Product search: realtime as user types (debounced 300ms)- Show stock indicator: ✅ in stock (qty) / ⚠️ low / ❌ out- Show last purchase price vs current list price → margin %- Serial number picker: if product.track_serial → show multi-select of available SNs- VAT rate auto-filled from product, overridable- Discount: per line, also supports per-client price list from price_lists collection- Line totals update live without server round-trips7. e-Factura Integration
Section titled “7. e-Factura Integration”flowchart TD
INV["Invoice confirmed\n(status = confirmed)"]
XML["buildUBLXml(invoice)\n→ UBL 2.1 XML string\n(TypeScript, ~200 lines)"]
VALID["ANAF UBL validator\n(optional pre-check)"]
TOKEN["getAnafToken()\nOAuth2 client_credentials\nusing company's SPV certificate"]
UPLOAD["POST to ANAF SPV\nhttps://api.anaf.ro/prod/FCTEL/rest/upload\nContent-Type: application/xml"]
STORE["Save upload_id\nto invoices_out.efactura_id"]
POLL["Poll ANAF every 5min\nGET /descarcare?id=upload_id\nuntil status = OK or NOK"]
DONE["Update efactura_status\non invoice"]
INV --> XML --> VALID --> TOKEN --> UPLOAD --> STORE --> POLL --> DONE
UBL XML structure for Romanian e-Factura:
export function buildUBLXml( invoice: Invoice, lines: InvoiceLine[], client: Tert, company: Company): string { return `<?xml version="1.0" encoding="UTF-8"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"> <cbc:UBLVersionID>2.1</cbc:UBLVersionID> <cbc:ID>${invoice.number}</cbc:ID> <cbc:IssueDate>${invoice.date}</cbc:IssueDate> <cbc:DueDate>${invoice.due_date}</cbc:DueDate> <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode> <cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode> <cac:AccountingSupplierParty> <cac:Party> <cac:PartyTaxScheme> <cbc:CompanyID>RO${company.cui}</cbc:CompanyID> <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme> </cac:PartyTaxScheme> <cac:PartyLegalEntity> <cbc:RegistrationName>${company.name}</cbc:RegistrationName> <cbc:CompanyID>${company.reg_com}</cbc:CompanyID> </cac:PartyLegalEntity> </cac:Party> </cac:AccountingSupplierParty> ${buildBuyerParty(client)} ${lines.map((l, i) => buildInvoiceLine(l, i + 1)).join('\n')} ${buildTaxTotals(lines)} <cac:LegalMonetaryTotal> <cbc:LineExtensionAmount currencyID="RON">${invoice.total_net.toFixed(2)}</cbc:LineExtensionAmount> <cbc:TaxExclusiveAmount currencyID="RON">${invoice.total_net.toFixed(2)}</cbc:TaxExclusiveAmount> <cbc:TaxInclusiveAmount currencyID="RON">${invoice.total.toFixed(2)}</cbc:TaxInclusiveAmount> <cbc:PayableAmount currencyID="RON">${invoice.total.toFixed(2)}</cbc:PayableAmount> </cac:LegalMonetaryTotal></Invoice>`;}8. Supplier Price Feed Imports
Section titled “8. Supplier Price Feed Imports”All 20+ IT distributors share the same pattern — only the auth/format differs.
interface SupplierAdapter { name: string; fetchOffers(): Promise<RawOffer[]>;}
interface RawOffer { supplierCode: string; price: number; stockQty: number; currency: string;}
// src/lib/suppliers/rhs.tsexport const RHSAdapter: SupplierAdapter = { name: 'RHS', async fetchOffers() { const res = await fetch('https://api.rhs.ro/pricelist', { headers: { Authorization: `Bearer ${env.RHS_TOKEN}` } }); const data = await res.json(); return data.products.map((p) => ({ supplierCode: p.cod, price: p.pret_fara_tva, stockQty: p.stoc, currency: 'RON' })); }};
// Same adapter for ELKO, MICRO, ROYAL, etc. — just different URL/auth/field names// pb_hooks/cron_suppliers.pb.js — runs nightlycronAdd('supplier_feeds', '0 2 * * *', () => { // for each active supplier with an adapter configured: // 1. fetch offers // 2. for each offer, find matching product_supplier_codes.supplier_code // 3. upsert supplier_offers record with new price + stock // this gives the "compare prices across distributors" view for free});9. What maxxcomputers Specifically Uses (Migration Checklist)
Section titled “9. What maxxcomputers Specifically Uses (Migration Checklist)”Modules — Keep (core business)
Section titled “Modules — Keep (core business)”- Produse — product catalogue with supplier cross-codes
- Terti — clients + suppliers (unified, typed C/F/CF)
- CFI (cfi/) — customer invoices →
invoices_out - FFI (ffi/) — supplier invoices →
invoices_in - Comenzi clienti — customer orders →
orders_out - Comenzi furnizori — purchase orders →
orders_in - Trezorerie — payments, cash register, cheques →
payments+payment_invoice_links - Stoc produse — stock movements + current stock (computed on product)
- Serial numbers — tracked on IT hardware →
serial_numbers - Garantii — warranty/service tickets →
warranties - Licente (rapoarte/licente/) — software license sales tracking →
licenses - Liste preturi — per-client price lists with discount % →
price_lists - BNR rates — daily currency update →
currencies+ cron hook - ANAF CUI lookup — auto-fill tert on create → ANAF REST call in form
- e-Factura — UBL XML + ANAF SPV submission →
efactura.tsutility - Supplier price feeds — 20+ adapters →
supplier_offers+ nightly cron - Coletarie / Innight — shipment AWB tracking →
shipments - PDF invoices — generate on confirm → file stored in PocketBase Files
- Email notifications — invoice PDF to client, payment confirmation
Modules — Drop (not relevant to a computer shop)
Section titled “Modules — Drop (not relevant to a computer shop)”-
Avicola / serii_crestere_pui— chicken farming -
Mobilier— furniture manufacturing -
Mifix— fixed assets (can add later if needed) -
Salarii / payroll— use dedicated payroll software -
Balanta / contabilitate— export data to accounting software (Saga, WinMentor) -
Productie / asamblare— assembly production line (add later if needed) -
Devize / situatii lucrari— project quoting (service orders module covers maxx) -
D394 / SAF-T— accountant’s job, out of scope for v1
Data to Migrate from Existing DB
Section titled “Data to Migrate from Existing DB”-- These tables from the maxx PostgreSQL DB map to new collections:terti → tertiproduse → productsproduse_terti → product_supplier_codesstocuri → initial stock record (one-time import + stock_movements seed)cursvalutarbnr → currencies (last 12 months only)garantii → warranties-- invoices: import last 2 years for reporting history, not older-- payments: import open (unpaid) balances only, not full history10. Architecture Decision Record
Section titled “10. Architecture Decision Record”| Decision | Choice | Reason |
|---|---|---|
| Database | PocketBase (SQLite) | Single tenant = one PB instance. Write serialization is a feature — prevents double-deduction of stock. No race conditions possible. |
| One instance per company | Yes | Better isolation than shared DB. Scale via Docker/Fly.io. Breach of one tenant can’t touch others. |
| Business logic location | PocketBase JS hooks only | Logic runs inside PocketBase transaction. SvelteKit routes are dumb — they only pass data. No split-brain possible. |
| Frontend framework | SvelteKit | Minimal boilerplate, excellent form actions, runs fine on Cloudflare Pages. |
| PDF generation | @react-pdf/renderer server-side in SvelteKit | Runs in Node/Cloudflare Workers, no headless browser needed for invoice-style PDFs. |
| Resend API | replaces the broken SMTP setup, 1 API call, reliable delivery | |
| Supplier feeds | Adapter pattern, nightly cron in PB hooks | Easily add/remove suppliers without touching core code. |
| Upgrade to PostgreSQL | When needed | Swap PocketBase DAO calls to a db.ts abstraction layer backed by Drizzle ORM. Hooks become PostgreSQL triggers or Supabase edge functions. SvelteKit routes are untouched. |
11. Technology Stack Summary
Section titled “11. Technology Stack Summary”graph LR
subgraph Dev["Development"]
TS["TypeScript"]
SVELTE["SvelteKit 2"]
TAILWIND["Tailwind CSS"]
SHADCN["shadcn-svelte\n(UI components)"]
end
subgraph Runtime["Runtime"]
CF["Cloudflare Pages\n(SvelteKit SSR)"]
PB["PocketBase\n(Fly.io / VPS)\nSQLite + JS hooks"]
end
subgraph Services["External Services"]
RESEND["Resend\n(email)"]
R2["Cloudflare R2\n(PDF archive)"]
ANAF2["ANAF APIs\n(CUI + SPV)"]
BNR2["BNR XML\n(rates)"]
end
Dev --> Runtime
Runtime --> Services
12. Folder Structure
Section titled “12. Folder Structure”maxx-erp/├── src/│ ├── routes/ (see §4 Route Map)│ ├── lib/│ │ ├── pb.ts — PocketBase client singleton + typed collections│ │ ├── db.ts — abstraction layer over PocketBase (swap for Drizzle later)│ │ ├── efactura.ts — UBL XML builder + ANAF SPV client│ │ ├── bnr.ts — BNR XML parser│ │ ├── pdf/│ │ │ ├── InvoiceTemplate.tsx│ │ │ └── render.ts — renderToBuffer wrapper│ │ ├── suppliers/│ │ │ ├── base.ts — SupplierAdapter interface│ │ │ ├── rhs.ts│ │ │ ├── elko.ts│ │ │ └── ... — one file per distributor│ │ ├── vat.ts — Romanian VAT computation (19/9/5/0, rounding rules)│ │ ├── document-number.ts — series + sequential number formatting│ │ └── anaf.ts — CUI lookup client│ └── app.d.ts — Locals type (user, company, pb instance)│├── pb_hooks/ — PocketBase JS hooks (business logic)│ ├── invoices_out.pb.js│ ├── invoices_in.pb.js│ ├── orders_out.pb.js│ ├── payments.pb.js│ ├── serial_numbers.pb.js│ ├── cron_bnr.pb.js│ └── cron_suppliers.pb.js│├── pb_migrations/ — PocketBase schema migrations (JSON, auto-generated)│└── scripts/ ├── migrate-from-ancora.ts — one-time data import from old PostgreSQL DB └── seed.ts — dev data seeder