back to work

Kalis

Period 2025–26Role Founding Engineer

A multi-tenant CRM for appointment-based businesses, covering scheduling, client records, staff permissions, payments, and double-entry accounting. Each business uses its own subdomain and isolated PostgreSQL schema.

Problem

Appointment-based businesses needed scheduling, client records, payments, settlements, and staff permissions in one product.

Contribution

I designed the system architecture, built the backend and frontend, and set up and managed the deployment infrastructure.

Result

Used by three businesses on dedicated subdomains.

System

A modular NestJS backend and Next.js frontend with schema-per-tenant PostgreSQL, Redis, double-entry accounting, and a read-only AI assistant.

Main challenge

Preserving tenant isolation and accounting accuracy as the product evolved.

Kalis tenant dashboard showing upcoming appointments, appointment summaries, finance totals, and client activity
Kalis finance dashboard showing pending collections, income and expense trends, cash distribution, and liabilities
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.

Technical deep dive
System topology — one deployable, many tenantsThe tenant applications and superadmin panel use the same NestJS API, which runs on a Hetzner VPS. Each request passes through tenant resolution, authentication, and authorization checks before reaching a domain module, and database access is scoped to the resolved tenant schema.
System topology — one deployable, many tenantsThe tenant applications and superadmin panel use the same NestJS API, which runs on a Hetzner VPS. Each request passes through tenant resolution, authentication, and authorization checks before reaching a domain module, and database access is scoped to the resolved tenant schema.Domain modules — one deployablereq / resadmin opsevery requestroutesTypeORM · schema-scopedcache · limitschat · mail · uploadsTenant web app*.kalis.com.trNext.js · VercelAdmin paneladministrator.kalis.com.trplatform administrationNestJS APIapi.kalis.com.trHetzner VPSRequest checkstenant resolution · authentication · authorization · isolationAppointmentsClients & intake formsStaff & commissionsPaymentsFinance & ledgerPackagesScheduler jobsAI assistantPostgreSQL · AWS RDSmanaged · backups · failoverpublic + tenant_N schemasRedis · ElastiCachetenant-resolve cacherate limits · hot readsProvidersOpenAI · MailerSendGoogle Cloud Storage
One request, one schema — tenant isolationThe subdomain identifies the tenant. Middleware looks it up in the public schema, selects the matching PostgreSQL schema through a pooled connection, and rejects cross-tenant access before the request reaches application logic.
One request, one schema — tenant isolationThe subdomain identifies the tenant. Middleware looks it up in the public schema, selects the matching PostgreSQL schema through a pooled connection, and rejects cross-tenant access before the request reaches application logic.requestresolvetenant contextscoped SQLenforcesbusinessname.kalis.com.trany tenant subdomainwildcard DNSTenantMiddlewareHost header → tenantx-tenant fallbackpublic schematenants · plansfeature flags · usersTenantConnectionpooled DataSourceper schematenant_businessname24 domain entitieshard isolationTenantIsolationGuardcross-tenant → 403
Where the money moves — double-entry ledgerEvery financial event is recorded as a balanced debit-credit entry in an append-only ledger built on a 10-account chart. Scheduled jobs automatically post POS settlements, credit-card payments, and salary or commission accruals on their configured dates.
Where the money moves — double-entry ledgerEvery financial event is recorded as a balanced debit-credit entry in an append-only ledger built on a 10-account chart. Scheduled jobs automatically post POS settlements, credit-card payments, and salary or commission accruals on their configured dates.Double-entry ledger — every event is a debit/credit pairA1/A2/A3 ↔ R1A3 → A2 + E3E2 → L1/L2 · L4 → A2aggregatesPaymentcash · card · transfercheckout at session endPOS settlementcommission rateT+N days to bankScheduler cronssettlements · CC duessalary & commission accrualAssets A1–A3cash · bankPOS receivableLiabilities L1–L4salary · commissionsupplier · cardRevenue R1service incomeExpenses E1–E3fixed · personnelPOS commissionFinance dashboardscash on hand · receivablesincome statement · payables
Appointment lifecycle — booking to settled cashThe system keeps each appointment traceable from booking and session notes through checkout, ledger entries, and POS settlement.
01Bookingconflict-checked slot
02Sessionnotes · therapist-only
03Checkoutdiscount · method · split
04Ledgerdebit / credit posted
05SettlementPOS T+N · automated
Key decisions & why
01

Modular 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.

02

Tenant 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.

03

Double-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.

04

Read-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.

05

Simple 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.

Stack
  • TypeScript
  • NestJS
  • Next.js
  • PostgreSQL
  • TypeORM
  • Redis
  • OpenAI API
  • Amazon RDS
  • Amazon ElastiCache
  • Hetzner
  • Vercel