Yuri Korolev
New York
iInfo
Yuri Korolev
New York
08

CTK

Web App2026
CTK — Web App

Context

CTK is an architecture firm, and this is the largest thing I have built through Korrit. They came wanting their timesheets and their billing to stop being two separate jobs done by hand, and what they described was a form: everybody types their hours in one place, somebody approves them, an invoice comes out of the other end.

The constraint was that both ends already belonged to somebody else. Their entire history sat inside Monograph, which is practice management software an architecture firm rents by the month, and their bookkeeping sat inside QuickBooks, which they were not going to leave and should not have to. So the useful version of this was never the form. It was getting the data out of the product that was renting it back to them and into a Postgres database with their name on it, then handing the invoice end back to QuickBooks and building only the middle.

The middle is a weekly timesheet with a row per project and a cell per day, an approvals queue that groups one card per person per month, a set of rules about when a finished month stops being editable, and a push into QuickBooks the moment a manager approves. There is also a chat box that answers questions about the database in plain English, which nobody asked for.

Details

Role
Developer, through Korrit
Team
Solo
Timeline
Mar — Aug 2026
Built with
TypeScript, Next.js, Postgres, QuickBooks API
Deliverables
Timesheet app, data migration, QuickBooks sync, monthly report
Status
Live at the firm, in support

Tech stack

App
Next.js·React·TypeScript
Data
Postgres·Supabase·Row level security
Billing
QuickBooks Online API
Ask the DB
Schema introspection·Read-only SQL
Scheduled
Vercel crons·Resend
Interface
Tailwind·Base UI

Final artifacts

CTK — A week of somebody's hours, before the month closes on them
A week of somebody's hours, before the month closes on them
CTK — A question in English, and the query it turned into
A question in English, and the query it turned into
CTK — One card per person per month, and QuickBooks behind the button
One card per person per month, and QuickBooks behind the button

Key design moments

01

The migration was a day of scripts and the whole point of the project

Everything CTK had was inside Monograph, and the only part of it they owned was an export button. So the first real work was three CSVs: the timelog, the projects with their phases and their clients, and the invoices. None of it carries an id you can join on. The timelog identifies a person by their full name spelled out, so the import opens with a hand written map of twenty names onto the profile rows I had just created, one of them with a diacritic in it that had to survive the trip. Several entries against the same person, day and project fold into one row, hours are capped at twenty-four because the table has a constraint saying a day cannot be longer than a day, and every row gets an id built deterministically out of the date and the person and the project, so running the import twice does not give the firm two of each year. Time entries went in eighty at a time and invoices a hundred. Then I deleted the scripts a fortnight later, which is what a migration script is for.

CTK — The migration was a day of scripts and the whole point of the project
The migration was a day of scripts and the whole point of the project
02

I built an invoicing page and then deleted it

For about three weeks in May this app could make an invoice. There was a modal, a date range picker I wrote from scratch because nothing I tried behaved, a filter that narrowed the line items down to the people with approved hours in the period, and a bulk flow for running several projects at once. On the thirteenth I wrote a spec for taking all of it out again, and by the end of that day the modal, the picker, the draft action and the QuickBooks invoice helper were gone. Their bookkeeping is in QuickBooks and it is staying there. What this app should hand over is hours and nothing after them. So approval now pushes one time activity per entry against a QuickBooks employee, a QuickBooks project and a service item taken from the person's role rather than from the project's name, and QuickBooks writes the invoice the way it already did. Making that push safe took three guards stacked on each other, and deciding what not to build took longer than all three.

CTK — I built an invoicing page and then deleted it
I built an invoicing page and then deleted it
03

Nobody should have to learn SQL to ask how the month went

There is a chat box in the admin sidebar called Ask the DB. You type a question and it answers it out of the live database. Underneath, the model is handed a description of the schema built by querying the schema: every table and column, every foreign key annotated with the column that holds the readable name behind it, and the distinct values of the low cardinality columns so it filters on the casing that is actually in there instead of guessing. The QuickBooks tokens are stripped out of that description before any of it is sent. It can call exactly one tool, which runs a single query inside a Postgres function that opens a read-only transaction with a five second timeout, and the SQL is checked before it gets that far: it has to start with select or with, it cannot contain a semicolon or a comment, and a list of keywords and file reading functions fails it outright. Three layers doing one job, on purpose, because the thing writing the query is not something I can reason about. What I did not expect was that the day after building it went entirely on the iOS keyboard covering the input.

CTK — Nobody should have to learn SQL to ask how the month went
Nobody should have to learn SQL to ask how the month went

Impact

−40%Time from approval to billing
25+People filling it in
39Tables in a database they own

The first two figures are CTK's rather than mine. They are the ones who timed the gap between a timesheet being approved and it being billed, and they are the ones who know how many people are in the building, so both are what I was told rather than anything I measured. The thirty-nine tables I counted off the schema, and it is the dullest of the three and the only one I can stand behind completely: their projects, their phases, their clients, their invoices and every hour anyone has logged now sit in a Postgres database that belongs to them instead of behind an export button on a subscription. What that opens is not something I can put a number on. It is why Ask the DB could be built in a day, why the monthly hours report exists at all, and why whatever they want next is a question about what to build rather than about what they are allowed to have.

Reflections

Writing the rules down is what found the bugs. Late on, the app got a Policies page stating in plain English how a month closes: it stays open through the Sunday ending the week that contains the first Monday of the next month, the hours report reaches the office manager the day after that Monday, and the lock always lands on a Monday between the eighth and the fourteenth. The page derives its worked example from the same function that enforces the rule, so the two cannot drift apart. Then I went looking for whether the code backed every sentence, and twice it did not. The lock guard took today from a plain date constructor, and Vercel runs its functions in UTC, so from eight in the evening Eastern the server already believed it was tomorrow and the month shut four hours early, inside the window the page had promised. Deleting an entry never checked the lock at all, only creating and editing did. A rule you have written down for users is a claim about your code, and I had made two I could not back.

The three weeks I spent on invoicing were three weeks spent rebuilding something the firm was already paying QuickBooks to do. I should have begun by asking which of the systems they already run is going to keep owning which part of this, and drawn those lines before writing anything, instead of finding them by deleting a fortnight of work. I made the same mistake smaller in April, building a whole guides section over two days and removing it in May. The lesson is waiting at the far end too: time off balances are meant to come from ADP, where payroll actually lives, and that integration is written up and not built, because reaching it needs CTK to buy a thousand dollar add-on and issue certificates from their own account. None of that is a coding problem and it has held the feature for months. Building the thing is fast enough now that building the wrong thing is the expensive part.