invoi.app

Von der Konzeption bis zum Deployment: alles aus einer Hand

invoi.app ist meine persönliche Buchhaltungsapp, die ich im Rahmen meiner Selbstständigkeit entwickelt habe. Als ich damals anfing, nach einer Softwarelösung für meine Buchhaltung zu suchen, kam in mir schnell der Gedanke auf, dass ich nicht für etwas bezahlen möchte, was ich auch selber bauen kann. Gedacht, getan. Diese App hat mit den Jahren verschiedene Evolutionsstufen durchlaufen und mir immer wieder Futter zum Lernen gegeben. Dabei ist jede Ebene durch meine Hände gegangen: Anforderungen, Datenmodell, Frontend, Backend, PDF Service und Infrastruktur. Aktuell beschäftige ich mich verstärkt mit der DevOps-Seite der App.

Architektur

Vom Interface bis zur Infrastruktur

invoi.app ist kein einzelnes Programm, sondern ein kleines verteiltes System. Mir war wichtig, API und PDF-Service voneinander getrennt zu halten. Die API betrachte ich als reine Zugriffsschicht auf die Datenbank, der PDF-Service ist davon komplett unabhängig und ließ sich daher gut abkapseln. Hier ist die ganze Landkarte auf einen Blick — jede Schicht, von der Oberfläche, in der ich Rechnungen anlege, bis runter zum Server, auf dem alles läuft.

Frontend

Das Interface

Die Oberfläche, in der ich Rechnungen anlege, Kunden verwalte und fertige PDFs herunterlade. Für die Umsetzung habe ich auf ein fertiges Template von nuxt/ui gesetzt, um das Rad nicht neu zu erfinden und ein solides Design zu haben.

Vue Nuxt TypeScript
Backend · API

Der Datenzugriff

Symfony war ein echter Game Changer. Sobald ich das Datenmodell über Doctrine abgebildet hatte, hat mir API Platform daraus alle grundlegenden CRUD-Operationen generiert. Auch um Authentifizierung oder die Validierung eingehender Requests muss man sich nicht selbst kümmern, das bringt Symfony von Haus aus mit. Die Entwicklung war dadurch sehr reibungsarm.

Symfony API Platform JWT
Daten

Das Datenmodell

Ein sauber normalisiertes Schema, über Doctrine abgebildet. In der ersten Version von invoi.app habe ich auf MongoDB gesetzt. Mit der Zeit wurde mir aber klar, dass sich für dieses Datenmodell eine relationale Datenbank besser eignet.

PostgreSQL Doctrine
Service

Der PDF-Dienst

Ein eigener kleiner Service mit nur einer Aufgabe: aus Rechnungsdaten ein sauberes PDF rendern. Die Entwicklung mit Hono war reibungslos und erfrischend.

Hono Playwright Handlebars
Betrieb · Infra

Das Fundament

aktueller Fokus

Container, CI/CD und Monitoring. Alles auf meinem eigenen Homelab statt in fremder Cloud. Das ist der Bereich, in dem ich aktuell am liebsten unterwegs bin.

Docker nginx MinIO Gitea Grafana
Datenmodell

Relational und normalisiert

Wie in der Architekturübersicht bereits erwähnt, bot es sich an, dieses Datenmodell relational umzusetzen. Besonders die Beziehung zwischen User und Organization lässt sich optimal über eine Verbindungstabelle abbilden. Über die Fremdschlüssel kann ich im Frontend gezielt filtern, etwa Rechnungen nach Kunde oder Organisation.

user7
PKidint
usernamevarchar(180)
rolesjson
passwordvarchar
firstnamevarchar(255)
lastnamevarchar(255)
emailvarchar(255)
organization5
PKidint
namevarchar(255)
streetvarchar(255)
cityvarchar(255)
zipvarchar(255)
template3
PKidint
titlevarchar(255)
urlvarchar(255)
organization_user4
PKidint
FKuser_idint
FKorganization_idint
roleenum
client8
PKidint
FKorganization_idint
namevarchar(255)
subtitlevarchar(255)
streetvarchar(255)
cityvarchar(255)
zipvarchar(255)
vatvarchar(255)
contact5
PKidint
FKclient_idint
firstnamevarchar(255)
lastnamevarchar(255)
birthdatedate
invoice17
PKidint
titlevarchar(255)
FKclient_idint
FKtemplate_idint
FKcontact_idint
invoice_nrvarchar(255)
statusenum
net_totalint
tax_totalint
gross_totalint
FKuser_idint
FKorganization_idint
invoice_datedate
service_period_startdate
service_period_enddate
payment_datedate
pdf_pathvarchar(255)
line_item7
PKidint
FKinvoice_idint
positionint
descriptionvarchar(255)
quantityint
unit_priceint
totalint
PK Primary KeysFK Foreign Keys8 Tabellen · 10 Foreign Keys · Beträge als Integer in Cent
Screenshots

Enough said, show me some screens

In Zukunft werde ich eine Live-Demo zur Verfügung stellen. Für den Moment, müssen Screenshots ausreichen.

invoi.app/dashboard
Die App in Aktion
invoi.app/invoices/create
Rechnungen anlegen
Ablauf

Wie eine Rechnung entsteht

Ein Rechnungs-PDF wird immer dann generiert, wenn im Frontend eine neue Rechnung angelegt wird.

Das Frontend schickt die Rechnung an die API. Die speichert sie in Postgres und fragt beim PDF-Dienst ein Dokument an. Der rendert aus einem Handlebars-Template mit Playwright ein PDF und legt es in MinIO ab. Zurück kommt eine Download-URL, die nur eine Weile gültig ist.

Diagramm wird geladen …
Bereitstellung & Betrieb

Vom Code zum Betrieb

Eine App zu schreiben ist die eine Hälfte. Sie so zu betreiben, dass ich ihr vertraue, ist die andere — und da lerne ich gerade am meisten.

Ehrlich gesagt war der Betrieb lange Murks. Secrets lagen im Image, die Container liefen als root, die Logs waren über drei Dienste verstreut (allein im Frontend über 70 Stück). Solange nur ich die App nutze, geht das. Aber ordentlich ist anders. Das räume ich gerade Schritt für Schritt auf — und aus jedem Schritt wird ein kleines Stück, das ich zeigen kann.

Diagramm wird geladen …

Durchgezogen läuft schon · gestrichelt ist geplant.

gitea/actions/ci
Grüne Pipeline
gitea/actions/build-and-deploy
Grafana-Dashboard
Ende der Tour

Danke fürs Durchscrollen. Weiter zu den anderen Projekten?

Zurück zu den Projekten