Bookkeeping tells you what you earned. Your bank tells you the balance. Neither tells you the number you actually care about: how much can you spend this month without running short on taxes later?
Avisio answers that. It's a financial planning tool for freelancers and small service businesses in Germany. Connect your accounts, set your tax and reserve rules once, and it keeps a running figure for what's safely spendable ("Verfügbarer Betrag"), alongside reserve coverage, net worth, and a year-end tax projection.
It is not a bookkeeping tool. No invoices, no receipt scanning, no VAT pre-registration. That's Lexware's job. Avisio sits on top and plans the future instead of documenting the past.
The built-in agent, Navi, works on your real numbers rather than generic advice. It explains why a figure moved, simulates a decision before you commit to it, flags risks like client concentration or a shrinking runway, and can build a dashboard widget on the spot when the standard view doesn't show what you need.
The full product vision lives in docs/VISION.md.
Bun is the only package manager and runtime. The app is Next.js 16 (App Router) with React 19.
- Database: PostgreSQL on Neon, accessed through Drizzle ORM. We run a vendored
drizzle-kitfork (vendor/drizzle-kit/) for virtual generated columns and non-interactive migrations. - API: tRPC for everything internal. REST is reserved for auth, webhooks, cron, OAuth, SSE streaming, and uploads. See
docs/CLAUDE.mdfor the full list. - Auth: Better Auth, including its OAuth provider.
- AI (Navi): Vercel AI SDK across Anthropic, Google, OpenAI, and Moonshot, plus MCP. Max-tier users bring their own API keys, encrypted with AES-256-GCM.
- Billing: Stripe, with Free, Pro, and Max tiers.
- Banking: Enable Banking for live balances and recurring-transaction detection.
- Accounting: Lexware sync for plan-versus-actual comparison.
- Money:
NUMERIC(15,2)columns andDecimal.jsarithmetic. Never floats. - i18n: next-intl, with German as the source locale and English alongside it.
- Testing: Vitest for unit and integration, Playwright for e2e.
- Observability: Sentry, PostHog, Pino.
- Bun 1.3 or newer
- Docker, running, for the local Postgres database
psqlandjqon your PATH. The setup scripts call both.
.env is checked in as a documented template. Copy it and fill in the gaps:
cp .env .env.localEverything reads from .env.local. The template lists every variable with a comment, but most are optional and only switch on a specific integration. The four below are validated at startup by lib/env.ts, so the server refuses to boot without them:
| Variable | Notes |
|---|---|
DATABASE_URL |
Postgres connection string. The local-DB script fills this in for you (see step 2). |
BETTER_AUTH_SECRET |
Generate one with openssl rand -base64 32. |
BETTER_AUTH_URL |
http://localhost:3000 for local work. |
NEXT_PUBLIC_APP_URL |
Same URL as above. Must be a valid URL or the parse fails. |
Everything else is optional and gates a feature when set. Leave it blank and that feature stays off:
| Group | Variables | What it unlocks |
|---|---|---|
| Encryption | MASTER_KEY_B64, MASTER_KEY_ID |
BYOAI key storage. Needed before a user can save their own AI key. |
| AI | DEFAULT_AI_API_KEY |
Platform-managed Navi on the lower tiers. |
RESEND_API_KEY, EMAIL_FROM |
Transactional email. Without it, verification mails are skipped. | |
| Billing | NEXT_PUBLIC_BILLING_ENABLED, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, the STRIPE_PRICE_* set |
Stripe checkout and the subscription pages. Run bun scripts/seed-stripe-products.ts to create the prices. |
| Banking | ENABLE_BANKING_ENABLED, ENABLE_BANKING_APP_ID, ENABLE_BANKING_PRIVATE_KEY |
Live bank connections. |
| Google login | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
The Google social provider. Redirect URI is $BETTER_AUTH_URL/api/auth/callback/google. |
| Observability | NEXT_PUBLIC_SENTRY_DSN, SENTRY_AUTH_TOKEN, NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST |
Sentry and PostHog. |
| Cron / Lexware | CRON_SECRET, LEXWARE_WEBHOOK_URL |
Scheduled jobs and the Lexware webhook. |
The fastest path is the bootstrap script. It allocates a port for your branch, starts the Docker containers, points .env.local at the local DB, and runs migrations in one go:
bun install
bash scripts/local-db-init.shThe script saves your current .env.local as .env.cloud the first time it runs, so keep your non-DB config (auth secret, API keys) in .env.local before running it. After that, the two files are how the env switcher tells local and cloud apart:
bun env:local # rewrite .env.local to use local Docker Postgres
bun env:prod # restore the cloud config from .env.cloud
bun env:status # show which one is activeIf you'd rather wire it up by hand, the Docker stack is a Postgres 18 container plus a local Neon HTTP proxy (the app talks to Postgres over @neondatabase/serverless):
bun docker:up # start Postgres + neon-proxy
bun db:setup # pre-migrate SQL, migrations, post-migrate SQLLocal Postgres has no role separation, so DATABASE_URL, DATABASE_URL_APP, and DATABASE_URL_SECRETS all point at the same database. In the cloud, DATABASE_URL_APP connects as a restricted role with row-level security enforced.
bun devbun dev runs behind portless on the avisio.lvh hostname, so open the URL it prints (https://avisio.lvh.me) rather than localhost:3000.
| Command | What it does |
|---|---|
bun dev |
Dev server |
bun build / bun start |
Production build, then serve |
bun typecheck |
tsc --noEmit |
bun test <file> |
Run tests for the files you touched |
bun test:unit / bun test:integration |
Targeted runs |
bun test:e2e |
Playwright |
bun lint |
ESLint |
bun i18n:check |
Catch missing translations |
bun knip |
Find unused code and dependencies |
| Command | What it does |
|---|---|
bun db:generate |
Generate migration SQL from the schema |
bun db:migrate |
Apply migrations |
bun db:setup |
Pre-migrate SQL, migrate, post-migrate SQL |
bun db:studio |
Open Drizzle Studio |
bun db:branch / bun db:branch:delete |
Manage Neon branches |
To change the schema: edit lib/db/schema/index.ts, run bun db:generate, review the SQL, then bun db:migrate. Migrations have to be idempotent. The vendored drizzle-kit is documented in docs/VENDORED_DRIZZLE_KIT.md.
app/ Next.js App Router. Pages under app/[locale]/, routes under app/api/
components/ React components
lib/ Core logic: db, auth, ai, financial-core, calculations,
reserves, trpc, stripe, enable-banking, mcp, and more
messages/ Translation catalogs (de.json, en.json)
docs/ Vision, architecture, plans, runbooks
scripts/ DB setup, env switching, key rotation, seeding
vendor/ The drizzle-kit fork
Some of these are enforced by hooks in .claude/hooks/. The rest live in CLAUDE.md.
- Money is
NUMERIC(15,2)plusDecimal.js, never floats. - Deletes are soft, via
deleted_at. Nothing is ever hard-deleted. - Every mutation filters on
userIdin itsWHEREclause. - Financial mutations are audit-logged, for GoBD compliance.
- UI text goes through
t('key'). No hardcoded strings. - Before building a component, check the shadcn registries for one that already exists.
CLAUDE.mdcovers the working rules and commands.docs/CLAUDE.mdis the docs map, plus the tRPC-versus-REST split and the secrets/RLS rules.docs/VISION.mdis the product vision and data model.docs/architecture/holds the topic-specific architecture docs.