SME Financial OS - Finanční operační systém pro malé firmy

Turborepo monorepo s Next.js 15, Prisma, Postgres. Kompletní dashboard, demo režim, česká lokalizace.

TurborepoNext.js 15PrismaPostgreSQL

Brief

Český malý byznys (řemeslníci, e-shopy, freelance studia, malé restaurace) typicky žongluje 5+ tooly:

  • účetní SaaS (Pohoda, Money S3, FlexiBee)
  • fakturační aplikace (Fakturoid, iDoklad)
  • výkaz tracker (často Excel)
  • cash-flow planner (znovu Excel)
  • payroll (externí účetní + email)

Každý nástroj má svůj login, svou export logiku, svoje formáty. Majitel firmy se v pondělí ráno přihlašuje na 5 míst, aby zjistil "kolik mám peněz a kolik mi přijde tento měsíc". Nemá čas se v tom hrabat - proto to přehazuje na účetní, která fakturuje 800 Kč/h za to, že to lepí dohromady ručně.

SME Financial OS sjednocuje read layer všeho. Nesnaží se nahradit účetní SaaS ani fakturák - sjednocuje data z těch existujících nástrojů a dává majiteli jeden dashboard: aktuální cash, prognóza, splatné faktury, daňové povinnosti, odhad rezerv. Plus demo režim, kde si to ověří bez registrace.

Shipnuté za 14 dní od kickoffu k live demo (pilot zákazník). AI usage 80 % - Claude Code napsal většinu boilerplate (Prisma schema, API routes, dashboard komponenty), já jsem dělal architecture decisions, validation logic a Czech tax edge cases.

Architektura: Turborepo monorepo

apps/
├── dashboard/         ← Next.js 15 (App Router, RSC)
├── api/               ← Hono on Vercel Functions, integrace inbound
└── marketing/         ← Astro static site, sales/landing
packages/
├── db/                ← Prisma schema, migrations
├── ui/                ← shadcn/ui based komponenty, Tailwind
├── integrations/      ← FIO, Komerční, Pohoda, Money S3 adaptéry
└── domain/            ← business logic (DPH, cash-flow forecast, výkazy)

Turborepo pipeline má cached build pro db/domain/ui (změny tam jsou rare), takže pnpm build na dashboardu trvá 18 s místo 70 s naplno.

Multi-tenant scoping v Prisma: každý record má orgId foreign key, queries jsou wrapped v withOrg() helperu, který vynucuje filtr a hází runtime error, když chybí. Žádný row-level security v DB - Postgres RLS jsem zvažoval, ale pro tým 1–2 dev je composing query helperem rychlejší a stejně bezpečné.

Demo režim: full sample data

Klíčový diferenciátor. SME zákazníci jsou podezíraví: nedají email a heslo nějakému novému SaaS, dokud neuvidí, že to skutečně funguje. Demo s real-looking daty (3 měsíce, 200+ transakcí, 12 faktur, 8 dodavatelů, 4 zaměstnanci v payrollu) řeší ten "show, don't tell" handicap.

// packages/db/prisma/seed-demo.ts
import { faker, fakerCS } from '@faker-js/faker';
import { prisma } from './client';
 
export async function seedDemoOrg() {
  const org = await prisma.organization.create({
    data: { id: 'demo-org', name: 'Demo Truhlářství s.r.o.', ico: '12345678' },
  });
 
  // 200 transakcí napříč 3 měsíce, realistické popisy a částky
  const txs = Array.from({ length: 200 }, () => ({
    orgId: org.id,
    occurredAt: fakerCS.date.recent({ days: 90 }),
    amountCzk: faker.number.float({ min: -45_000, max: 280_000, multipleOf: 0.01 }),
    counterparty: fakerCS.company.name(),
    category: faker.helpers.arrayElement(['materiál', 'mzdy', 'energie', 'služby', 'tržby', 'splátka']),
    bankRef: faker.string.alphanumeric(10),
  }));
  await prisma.transaction.createMany({ data: txs });
 
  // 12 faktur, 4 přijaté, 8 vydané, 3 po splatnosti
  await prisma.invoice.createMany({ data: generateInvoices(org.id) });
 
  return org;
}

Demo je ephemeral - sezení přiděluje cookie session ID, mapuje ho na demo orgId. Po 24h cleanup job vymaže demo rows. Bez signupu, bez kreditky. CTA na konci demo pageu: "Chceš stejný dashboard pro svou firmu? Připojit svůj účet" → real signup.

Bank statement integrace

Tři způsoby, jak zákazník dostane data dovnitř:

Banka / ToolMethodFrekvence
FIO Bankatokeny pro REST APIkaždé 2 h
Komerční BankaOAuth2, Open Banking PSD2denně
Ostatní (CSOB, Moneta...)manuál CSV uploadad-hoc
Pohoda exportXML importtýdně
Money S3 exportXML importtýdně

FIO API je nejlepší - token-based, JSON output, žádný throttling. Komerční Banka vyžaduje plný OAuth2 flow s redirect URI a token refresh každých 30 minut, ale Open Banking standard je předvídatelný. Manuál CSV je fallback pro zbytek.

// packages/integrations/src/fio.ts
export async function syncFio(orgId: string, token: string, since: Date) {
  const url = `https://www.fio.cz/ib_api/rest/periods/${token}/${formatDate(since)}/${formatDate(new Date())}/transactions.json`;
  const res = await fetch(url);
  if (!res.ok) throw new IntegrationError('fio', res.status);
 
  const data = await res.json();
  const transactions = data.accountStatement.transactionList.transaction.map(mapFioToTx);
 
  // upsert podle bankRef (idempotentní - re-sync nevytvoří duplicity)
  await prisma.$transaction(
    transactions.map((tx: Tx) =>
      prisma.transaction.upsert({
        where: { orgId_bankRef: { orgId, bankRef: tx.bankRef } },
        create: { orgId, ...tx },
        update: { ...tx, updatedAt: new Date() },
      }),
    ),
  );
}

upsert na composite key (orgId, bankRef) je critical - kdybych použil pure createMany, re-sync po crashi by vytvořil duplicates a cash-flow forecast by lhal.

Cash-flow forecast

Hlavní value-add feature. Dashboard ukazuje 90-day forecast chart založený na dvou vstupech:

  1. Recurring transakce (energie, mzdy, leasing) - detekované přes z-test na měsíční částku ± 5 %
  2. Splatné faktury - projekce na due date, s paymentLikelihood faktorem podle historie odběratele
// packages/domain/src/forecast.ts
export function forecast90d(orgId: string): CashFlowForecast {
  const recurring = detectRecurring(orgId); // monthly patterns
  const invoices = getOpenInvoices(orgId);
  const today = startOfDay(new Date());
 
  const days: ForecastDay[] = [];
  let runningBalance = getCurrentBalance(orgId);
 
  for (let i = 0; i < 90; i++) {
    const date = addDays(today, i);
    let dayDelta = 0;
 
    for (const r of recurring) {
      if (matchesDayPattern(r, date)) dayDelta += r.amountCzk;
    }
    for (const inv of invoices) {
      if (isSameDay(inv.dueDate, date)) {
        dayDelta += inv.totalCzk * inv.paymentLikelihood;
      }
    }
 
    runningBalance += dayDelta;
    days.push({ date, delta: dayDelta, balance: runningBalance });
  }
 
  return { days, lowPoint: minBy(days, 'balance'), netChange90d: last(days).balance - getCurrentBalance(orgId) };
}

Není to ML - je to deterministic projekce. Pro malé firmy stačí, protože jejich cash-flow má rytmus (mzdy 15. a 25., DPH 25., energie 10.) a paymentLikelihood per-counterparty stačí v 90 % případů.

Česká lokalizace

Tohle je důvod, proč Czech-built SaaS překonává Stripe Atlas-y: nejde jen o překlady, ale o DPH semantics, formats, edge cases.

  • DPH 21 % / 12 % / 0 % sazby (od 2024 změna z 15 → 12). Cash-flow musí umět DPH-payer (přiznání kvartálně/měsíčně, povinný odvod 25.) i non-payer (vydělím / přidělím sazbu, ale DPH neodvádím).
  • Faktury formát: variabilní symbol max 10 cifer, podpora SPD QR (CZ standard).
  • Číselné řady faktur: YYYYMMDD-NN (20260203-01), nebo legacy YYYY/NNN (2026/001). Helper nextInvoiceNumber(orgId, format) v domain/.
  • Mobile-first. SMB majitel kontroluje finance na mobilu, sedíc v dodávce na štaci. Dashboard na 360px width je primary breakpoint, desktop je secondary. Stats cards stack vertikálně, charts simplified.

Performance budget

MetricTargetNaměřeno
Dashboard p95 (12 měsíců dat)< 1 s820 ms
RSC streaming first byte< 200 ms145 ms
Server action mutace (invoice update)< 500 ms240 ms
Forecast compute (90 dní × 12 měsíců trans.)< 300 ms180 ms

RSC + Server Components znamená, že prvních 5 stats cards (cash, faktury splatné, DPH závazek, mzdy) renderují server-side a streamují, zatímco chart komponent (klient-side recharts) se hydrátuje jen pro interaction. Žádný useEffect-fetch dance.

Lessons

  • SMB nedůvěřuje polished UI. První mockup byl příliš "Linear/Vercel-pretty" - pilot zákazník mi řekl "vypadá to drahé, kolik to bude měsíčně". Stáhl jsem aesthetic do utilitarian, blue/grey/green palette, větší fonty, méně whitespace. Přesvědčil mě řemeslník, který říkal "Pohoda je ošklivá ale spolehlivá, vy vypadáte jak startup, kterému se za 6 měsíců dojdou peníze."
  • DPH non-payer flag je load-bearing. Polovina pilotních zákazníků nebyla plátcem DPH, takže celá UI vrstva pro "DPH závazky" musí umět skrýt. Špatně designed boolean flag zlomí 5 částí appky najednou.
  • Demo > freemium. Freemium tier (5 transakcí free) byl první nápad. Demo s plně populated daty + "no credit card needed" konvertuje 3× líp, protože odstraňuje any signup friction. Kdo chce real účet, klikne až po tom, co viděl hodnotu.
  • Mobile-first changes API design. Server actions místo client fetch + useState dance jsou klíč k mobile p95 - méně JS, méně bundle, méně waterfalls. App Router + RSC tu byl zcela správná volba.
  • 80 % AI usage neznamená 80 % automatizace. Claude napsal většinu kódu, ale architecture decisions (mono vs micro, RLS vs query helper, demo strategy) jsem dělal já. AI ti dá rychlost, ne směr.