Tabloto: condominium management software for Bulgarian buildings
7 min read
Tabloto runs a Bulgarian apartment building's meters and money from a phone. It is named after the табло, the notice board in the entrance where the committee pins its reports and who owes what. The caretaker photographs each meter, the committee (домсъвет) approves the month, and every owner is invoiced automatically. Owners see their own invoices, their payments and the building's funds. I designed, built, host and support it on my own (more about me). This page is about how it's built: the decisions behind it, why they were made and what each one prevents. What it does for a building is covered in the full help guides on tabloto.app ↗ (opens in a new tab).
How it's built
Five decisions carry most of the weight. Each one is written the same way: what was decided, why, and what it prevents.
Architecture
- Phone or computer: the mobile web app, in React.
- Django API with a typed contract, hosted in Frankfurt.
- PostgreSQL and file storage, hosted in Frankfurt, each building's data isolated.
- Outside services: email (Brevo), AI providers behind one interface, card payments (Stripe, Premium).
Decision: a mobile web app in React and TypeScript, a Django API and PostgreSQL. Why: the hard part of Tabloto is business logic (tariffs, invoice runs, confirming readings, credit notes, reminders, invoice states), and that belongs in testable Python rather than in database policies and triggers. The managed database stays just a database, so there is no lock-in. A separate React app, rather than server-rendered pages, gives full control of the camera screens, at the cost of a second codebase. The API's typed contract generates the browser's client code, and CI checks it, so a backend change that would break the frontend fails the build. The typed client is what offsets that cost. Routes stay thin: the logic lives in service functions tested without HTTP, and configuration comes entirely from environment variables, with no code difference between environments. Email goes out through Brevo, PDFs are rendered with WeasyPrint, and the app is hosted in Frankfurt.
What it prevents: business rules scattered across layers, a frontend and backend that silently disagree, and code paths that exist in only one environment.
One codebase, every building isolated
Decision: one codebase serves every building, and each building's data is isolated in the data-access layer. Why not the database's own row-level security: the app connects to the database as a single service user, so the database can't tell residents apart. Instead, the data-access manager fails closed: it returns nothing unless a query is scoped to a building. A CI lint catches unscoped queries, an unscoped query blocks a release, and every feature ships with a test proving that one building can't see another's data. A separate database schema per building was rejected as premature. Roles narrow access further: partners administer only the buildings they created, and a management company sees only what an owner grants it, per flat and per right, until the grant is withdrawn. The one screen that does cross buildings, the shared support board, shows a field-by-field public summary of each ticket, never the ticket itself, because a bug report about the arrears screen naturally shows the neighbors' names and debts.
What it prevents: the one bug a multi-building system cannot afford, a query that shows one building's people and money to another.
Money that always reconciles
Decision: money is stored as whole cents, and floating-point numbers never touch it. Balances are calculated from an append-only ledger and never stored. Once an invoice is sent its money never changes: only its payment status moves, through two audited steps, paid and overdue, and a correction is a separate credit note. Each fee type maps to a fund when the building is set up, and a payment is split across funds in proportion to the invoice lines, because the law keeps those funds separate. Why numbering matters: in a Bulgarian court claim for unpaid fees (заповедно производство), a gap or a reused invoice number undermines the building's record. So numbers run per building, are assigned at approval rather than at draft, and come from a locked counter, because the database's own sequence doesn't roll back when an approval fails. A cancelled invoice keeps its number. With card payments, the building is the merchant of record and Kerl Vision never holds owners' money; the card fee is booked as the building's expense.
What it prevents: balances that drift, an edited invoice that an owner has already received, and a numbering gap that weakens the building's case in court.
AI that suggests, people decide
Decision: the AI suggests, a person decides. When the caretaker photographs a meter, the AI pre-reads the digits, a person confirms them, and the photo is kept as proof. The suggestion is stored once and never auto-approved. Why: a reading becomes an invoice, so a person stays accountable for every value. The AI receives the image only, with no owner, flat, building or expected serial number, and a test asserts that the request carries nothing else. If it fails or can't read the photo, the person types the value, so the meter round never depends on the model being available. Where the decimal point sits is a property of the meter, set by a person, not guessed per photo. Providers sit behind one interface: switching is configuration, not code, and tests use offline stand-ins. Expense capture follows the same rule: AI pre-fills a photographed supplier invoice, a person confirms it, and bank account numbers and company IDs are never stored. The expense-capture AI runs on Mistral in France.
What it prevents: a wrong reading reaching an invoice unseen, personal data travelling with a photo, and an AI outage stopping the monthly round.
How work ships
Decision: nothing is built without a written spec, and work ships in numbered stages, each closed with a tagged release. Backend and frontend tests run in CI on every push, and every screen that submits data has a test checking the full payload it sends. When a stage closes, an automated browser audit opens every screen at phone and desktop width, in Bulgarian and English, for every role, and checks for broken layouts, untranslated text and errors. Scripted walk-throughs drive each changed flow, and anything that needs human eyes goes on a QA checklist that must be current before a stage closes. Why: a test only counts once it has been seen to fail, so results are measured on the exact commit being released and the key test is shown failing without the fix. A staging server on demo data updates on every push to main, and the demo building is fenced off: its email goes nowhere, its AI and payment providers are stand-ins, and demo accounts can't change their login, delete themselves or join a real building.
What it prevents: green tests that test nothing, a release that differs from what was checked, and demo traffic reaching real people.
What it looks like in use
The screenshots show a demo building with invented flats, owners and meters, in Bulgarian and English.
Meter readings from a photo
The caretaker photographs each meter, and the committee reviews only the readings the app flags. Electricity, water and heating meters are supported.
One action invoices the whole building
Once the month is approved, every owner is invoiced in one action and the PDFs go out by email. Billing can be fixed, a split of the supplier's bill by measured consumption, or individual.
What owners see on their phone
Each owner sees their own invoices, what is paid and what is overdue, and can download every PDF. The invoice PDF is bilingual, with Bulgarian and English side by side.
Who owes what, and reminders
The finance view shows the funds, who owes what and a CSV export; reminders go out at most once per invoice per month. Unpaid invoices are marked overdue.
Readings and news for every owner
Announcements reach every owner in the app and by email, and people join the building through a single-use invitation link. Emails and PDFs go out in the recipient's language.
Start from the spreadsheet you already have
Committees usually keep the building in Excel already, so onboarding starts there. Download the template, fill in flats, owners and meters, and upload it for the app to validate.
For partners and property managers
Partners see only the buildings they created; a management company sees only what each owner grants it. Each building is administered separately.
What this project shows
- Full-stack ownership: one person designed, built, hosts and supports the mobile web app, the API and the database, with no hand-offs.
- Domain modelling under ЗУЕС: the separate funds, cost-splitting rules and paperwork of Bulgarian condominium law, built into the data model.
- Financial correctness: money in whole cents, balances derived from an append-only ledger, invoices that never change once sent, and numbering that holds up as evidence in court.
- AI with guardrails: the model suggests, a person decides, the photo is all it ever sees, and the meter round works even when it is down.
- Release discipline: specs first, tests that are shown to fail, and an audit of every screen in both languages before a release.
What's next
On the roadmap: cash payments through EasyPay and ePay.bg (research comes first), formal online voting with electronic signatures, bank reconciliation, and alerts for leaks and unusual consumption.
Want something like this?
Tabloto is one product, but the approach travels: a clear spec, a well-tested system, and screens that work on the phone people already carry. If your business runs on spreadsheets, paper and reminders sent by hand, this is the kind of work I do for clients. If you are hiring, “How work ships” above is the section I'd point you to.