← back to workProblemAppointment-based businesses needed scheduling, client records, payments, settlements, and staff permissions in one product.
ContributionI designed the system architecture, built the backend and frontend, and set up and managed the deployment infrastructure.
ResultUsed by three businesses on dedicated subdomains.
SystemA modular NestJS backend and Next.js frontend with schema-per-tenant PostgreSQL, Redis, double-entry accounting, and a read-only AI assistant.
Main challengePreserving tenant isolation and accounting accuracy as the product evolved.
Overview
Kalis evolved from a scheduling tool into a broader operational and financial system. We stopped onboarding new customers after deciding that the time required to grow and support the product outweighed the return.
Multi-tenancy
The middleware looks up the subdomain in a shared public schema, attaches the tenant context to the request, and selects a pooled connection for that tenant's PostgreSQL schema. JWT, role, and tenant-isolation guards reject requests whose authenticated tenant does not match the resolved subdomain before any domain logic runs.
The onboarding flow creates the tenant schema, runs migrations, seeds the chart of accounts, and activates the business's subdomain with a 14-day trial.
Money and ledger
Each financial event creates a balanced debit-credit entry in an append-only ledger built on a 10-account chart. Cash payments are recorded directly against revenue, while card payments create a POS receivable together with the commission and T+N settlement date.
Scheduled jobs automatically settle POS receivables, record commission expenses and credit-card payments, and accrue salaries or commissions on each staff member's configured payment date. Cash, receivable, payable, and income-statement views are generated from the resulting ledger entries.
Product surface
The weekly calendar is organized by staff member in 15-minute slots and supports drag-and-drop rescheduling, appointment statuses, and therapist-only notes. Client profiles bring together sessions, payments, packages, and structured intake forms. Owners, secretaries, and therapists see different operational and financial information based on their roles.
AI assistant
The assistant maps Turkish questions to one of more than 15 predefined intents, runs the corresponding read-only query within the user's permissions, and returns the result in natural language. For example, a therapist asking about revenue can see only their own figures.
The model does not generate SQL or write to the database. It can only choose from predefined read operations that use the same permission checks as the application.
Operations
The Next.js frontend is deployed on Vercel with wildcard subdomains, while the NestJS API runs on a Hetzner VPS. Tenant schemas are stored in Amazon RDS for PostgreSQL, and Amazon ElastiCache for Redis is used for tenant lookups, rate limiting, and frequently accessed dashboard data.
Public and tenant migrations run separately during provisioning. MailerSend handles transactional email for each tenant, while Google Cloud Storage stores uploaded images and their resized versions.
Key decisions & why
01Modular monolith
Core workflows can update appointments, packages, payments, and ledger entries in the same transaction. Keeping the system as one deployable preserves atomic writes and simplifies operations, while more than 20 NestJS modules maintain clear boundaries for future extraction.
02Tenant isolation
A separate schema for each tenant avoids relying on every query to include a tenant ID. Subdomain resolution, authentication, authorization, and tenant matching all run before the request reaches a controller, while a single RDS instance keeps migrations, backups, and monitoring manageable.
03Double-entry ledger
Balanced, append-only entries make every balance traceable and allow POS settlements, card payments, salaries, and commissions to follow the same accounting model. Adding this structure after customer data had accumulated would have been much riskier.
04Read-only AI assistant
The assistant maps each question to a predefined intent and runs a read-only query within the user's permissions. It may choose the wrong query if it misinterprets a question, but it cannot modify customer data.
05Simple compute, managed data services
The API runs on a Hetzner VPS because the workload does not justify Kubernetes. Amazon RDS and ElastiCache handle the stateful services, while Vercel and wildcard subdomains let new tenants use the same frontend deployment.