The everyday user guide
The TPA Ops playbook.
Every module, every field, every workflow — in plain English, with no jargon. Whether you're a partner scanning P&L, a PM updating progress, or a new joiner trying to figure out where to click, start here.
01 — Welcome
What is TPA Ops?
TPA Ops is the single place where the firm tracks everything that touches money and work. If it was in a spreadsheet before, it lives here now.
TPA Ops replaces a sprawl of Excel files, Google Sheets and informal tracking with one opinionated platform. It's where we record every contract we sign, every project we deliver, every milestone we bill against, every rupee that comes in, and every decision we make about who gets paid what.
The goals
| No hunt | Every number lives in one place. No more emailing around asking "what's the latest figure for X?" |
| One truth | The Partner's P&L, the PM's weekly update, and Finance's receipt entry all feed off the same data. |
| Paper trail | Anything money-related has an audit log. Who changed it, when, and why. |
| Opinionated | The platform nudges you to do things the right way. Fee caps, reminders, approval gates. |
- Project Lead → Project Owner. Renamed end-to-end (column on landing, filter, edit dialog, project detail chip, receivables column, import module). The role is unchanged — same multi-assign coordination/delivery flag. The rename disambiguates from sales leads (the Leads module).
- Project Detail → new Details tab. Area & Rate, General Info (site, city, description, expected close), and Project Owners moved out of the cramped header strip into a dedicated tab — second position after Overview. Same data, properly structured Cards.
- Payment Schedule → Pending column + rollup. Each milestone now shows Pending (₹) = Fee − Received. A header strip above the table shows Received · Pending · % Billed for the whole project.
- Projections page is now info-rich (see §9 Projections). New columns: % Complete (links to Tracker), Billed%, Pending, inline editable Confidence. Click any row to expand → per-milestone breakdown (Fee / Compl% / Received / Pending + totals). New "+ Add raise" button captures a raise inline. Existing raises can be Dismissed when realized; a soft Likely realized badge auto-flags raises where receipts already cover the amount.
- Three deep-links land at the Project Tracker. Click Completion % in Project → Payment Schedule, or Update progress on a meeting's per-project block, or % Complete in Projections — all jump to the Tracker with the right project auto-expanded and a "Back to..." link at the top.
- Sales Leads pipeline removed from /projections. Sales leads belong on /leads, not on the revenue-forecast page. Conflating the two was confusing — now they're separated by intent.
- Lead form gained Form filled by + master-driven Source of lead. "Form filled by" is the person who recorded the inquiry (distinct from the Associate who'll own it). Source picks from the Lead Source master (seeded with Website Form + Email Inquiry).
- Contract ↔ Project linkage works on real data (hot-fix from 10 May). The "Link existing" dropdowns on both sides — Contract → Project and Project → Contract (new button) — were previously broken or missing. Both directions now work, status-independent.
- Department + Designation are now managed dropdowns at Masters → Department / Designation. Hard-validated on bulk import — seed values before importing employees.
- Home Offering retired. Cross-offering AP/PA rule is gone. Commission-role eligibility is now decided by the Honorary checkbox alone.
- Honorary checkbox added to Create + Edit Employee dialogs.
- Area (Sq Ft) and Per Sq Ft Rate moved from Contract → Project. They were per-service metrics misplaced at the contract grain. Set on each project after creation.
- Internal Code visible on the contract form. Optional second identifier for TPA's own short-ref / helen-code-style mapping. Defaults to Contract ID when blank.
- Contract attachments — View + Download buttons on every attachment row. Previously Upload + Remove only.
- Project status hard-validated against the master at import time. Pre-fix, an "On hold" Excel value would 500 at commit; now it surfaces as a clear row error at validate time.
- Project edit page gained a General Info inline-edit block (Site location / City / Description / Expected close date) and an Area / Rate inline-edit block.
- Percentage convention unified (15.0000 = 15% literally everywhere). Inputs accept 0–100 across all pct fields.
- Vestigial fields dropped from the import templates:
annual_gross,ctc,monthly_gross(employees);currencyandscope_summary(contracts).
02 — Mental model
The big picture in 60 seconds.
TPA's work is shaped like this. If you understand this one diagram, the rest of the platform falls into place.
Schedule
Reading left to right:
- A client wants work done. They sign with TPA.
- A contract captures the deal — what services, how much money, when signed, for how long.
- Each service becomes a project. If a contract covers Architecture and Interior, that's two projects.
- Every project builds a payment schedule — the list of milestones we bill against, each with a fee.
- When money comes in, it's recorded as a receipt, which fills up the payment schedule from the top down.
Everything else on the platform — Receivables, Payout, Commission Ledger, Status Updates, Approvals, Meetings — supports this core flow.
03 — Orientation
Your first login.
Here's what you're looking at when you land on TPA Ops for the first time.
The left sidebar
The main menu. Every module is one click away. The items you see depend on your role — a super admin sees everything; a PM with only status-update rights sees a much shorter list; Finance sees money modules the PMs don't.
Top-right corner
A bell icon 🔔 sits pinned in the top-right of every page. Click it for notifications — things that happened you might care about, like approvals pending or receipts received. It auto-refreshes every 30 seconds.
Bottom-left of the sidebar
Your name, a theme toggle (light/dark), and a sign-out button. Always pinned at the bottom even when the nav is long.
Forgot your password
The login page has a Forgot? link next to the password label. Click it, type your email, hit Send reset link. Supabase emails you a recovery link within ~1 minute. Click it, set a new password, you're back in. No need to bother an admin for routine resets.
If your admin set you up with a temp password (e.g. TpaOps@<your-e-code>), the recommended first action is to log in once with the temp, then immediately use Forgot to set something memorable. Note that there isn't yet an in-session "change my password" page — the Forgot flow is the canonical path either way.
04 — Super admin
One-time setup.
Three things to seed before anyone uses the platform in anger. Do this once, then forget about it.
4.1 — Dropdown master
Go to Masters in the sidebar. This is where the lists that appear in every dropdown on the platform live.
There are seven categories:
| department | New (May 2026). The org-axis label on every employee. Replaced the retired home_offering field. Examples: Architecture, Interior, Municipal Liaison, Site, PMO, Finance, HR, External. |
| designation | New (May 2026). Job titles. Pre-seeded with the historical TPA list (Partner, Senior Director, Senior Director Architecture, Senior Director Interior, Director, Principal Associate, Associate, Junior Associate, Architect, Interior Designer, Draftsman, Site Engineer, Senior Architect, Admin, Senior Partner, Junior Partner, Business Development Head, Senior). Add anything else your roles need. |
| site_location | Cities and areas: Mumbai, Pune, Bangalore, Goa, Navi Mumbai, Thane. |
| project_type | The kind of work: Residential, Commercial, Hospitality, Institutional, Mixed Use, Educational. |
| contract_status | Lifecycle states: Draft, Active, On Hold, Closed, Terminated. |
| project_status | Project states: Active, On Hold, Completed, Cancelled, Terminated. |
| lead_source | Where new client enquiries come from. |
Each option has a label (the text everyone sees) and an optional colour (shows as a badge border — great for status categories so red means terminated, green means active, etc.).
4.2 — Employees
Go to Employees. Add everyone on payroll. This is the canonical employee master — one row per person, always.
For each employee:
| E-Code | TPA's internal employee ID. |
| Full name | As it appears on payroll. |
| Designation | Senior Director, Director, Principal Associate, etc. — pulled from the Designation dropdown master. Hard-validated: unknown values are rejected with a hint pointing at the master. |
| Department | New (May 2026). The org-axis label. Pulled from the Department dropdown master. Replaced the retired Home Offering / cross-offering rule — Department is informational; commission-role eligibility is now decided by the Honorary flag alone (see below). |
| Honorary | New (May 2026). Checkbox on Create + Edit. When checked, the employee cannot hold any commission role (AP / PA / Finder / Convertor). Use for advisors, retired partners on courtesy mention, anyone you want visible in the directory but never paying commission against. |
| DOJ | Date of joining. |
| Monthly salary | The canonical compensation number. Drives the salary-period table and feeds PNL salary cost calculations. Auto-seeds the first salary period on create with effective_from = DOJ. |
| Default commission % | The person's house rate — used if no per-contract override is set. Now stored as a percentage scalar (15.0000 = 15%) post-migration 0071. Bounds enforced at 0–100. |
| Payout category | New (Apr 22). One of: pnl (partner profit share, flag-only), mgp (minimum-guarantee employee), variable (ad-hoc bonus), normal (vanilla commission via contract roles), or blank. Set via the radio dialog on the employee edit screen. Drives how the Payout and Commission Ledger modules treat this person. |
| Is honorary | Tick if they're on fixed salary only, no commission. Honorary employees cannot hold any commission role on any contract — the platform enforces this. |
| Email & platform access | Separate from payroll email. When a super admin provisions platform access (see Admin), the email here becomes the login. Without provisioning, the employee exists on the master but can't sign in. |
Once dropdowns and employees are seeded, you're ready for real work.
05 — The loop
The core workflow.
The day-to-day rhythm. Seven steps that repeat forever.
- A client signs a contract. Someone at TPA creates the contract in the platform with the terms, value, and services.
- The contract covers one or more services. Each service needs a project. One contract = one or more projects.
- Projects and contracts get linked. The platform tracks which contract funds which project and how much money is allocated.
- Each project builds a payment schedule. Milestones like Concept Design, Schematic, DDs, CDs — each with a fee and a due date.
- Work happens. The PM updates progress weekly and marks milestones as in_progress or completed.
- Money comes in. Recorded as receipts, which fill the payment schedule from the top down.
- At period-close, Finance reconciles. The commission ledger closes for the period. Payouts for PNL / MGP / Variable / normal employees get settled based on that period's receipts.
Every step writes to the audit trail. You can always go back and see who did what, when, and why.
06 — Module deep dive
Contracts.
The legal document with the client. The starting point of everything else.
The list view
Go to Contracts in the sidebar. You see every contract in one table:
| Contract ID | A manual free-text ID. TPA's internal code like 2425/254/RJV/YJN or anything else you want. |
| Client | The company name. |
| Location | Site location. |
| Status | Draft, Active, On Hold, Closed, or Terminated. Colour-coded via the Dropdown Master. |
| Offering | Architecture, Interior, Municipal — one or more. |
| Project Type | Residential, Commercial, Hospitality, etc. |
| Contract Value | Total in rupees. |
| Signed | Either a date badge (if signed) or "Not signed". |
| Area / Rate | Square feet and ₹/sq ft. |
| Start / End | The contract period. |
Above the table: a search box for client names, filters for status and service, and a sort widget where you pick any column and direction.
Creating a new contract
Click New contract in the top-right. The form has these fields:
| Contract ID | Required. Type anything. No format enforced. Just needs to be unique. |
| Internal Code | Optional. TPA's own short identifier (helen-code-style mapping). If blank, defaults to Contract ID. Useful when the client's PO number and your internal reference differ. |
| Client company | Free text. No master client list to maintain first. |
| Location | Pick from the dropdown or type a new one to add it to the master list on the fly. |
| Project type | Same dropdown behaviour as location. |
| Offering | Click to toggle Architecture, Interior, and/or Municipal. Pick one or more. |
| Contract Value | Total deal value. |
| Start / End / Signed dates | Self-explanatory. The signed date is how the platform knows a contract is signed — it's separate from status. |
| Status | Usually draft until signed, then active. Values come from the Project Status / Contract Status dropdown master. |
| Client Reference No. | If the client uses their own PO number, put it here. Optional. |
| Remarks | Scope summary, special terms, gotchas, anything freeform. |
Click Create. The contract lands in the list.
Moved to the project module (May 2026): Area (Sq Ft) and Per Sq Ft Rate used to live on the contract. They're now per-project — a contract may bundle Architecture + Interior in different areas, so sqft can't sit at the contract grain. Set them on each linked project after creation.
The contract detail page
Clicking a contract opens its detail page with three tabs:
Overview tab
An editable form with everything you entered. The Save button lives inside the card top-right — changes don't save until you click it. At the bottom: Attachments. Upload PDFs, Word, Excel, or images. Pick the kind — Original / Amendment / Addendum / Other. Each attachment row has three buttons:
- View — opens the file in a new tab so you can preview without downloading.
- Download — saves the file to your machine with the original filename.
- Remove — appears on hover; deletes the attachment after a confirmation.
Projects tab
The key bit. Shows every project linked to this contract with their allocated value and status.
You can:
- Link existing — pick any project from the full list (no status filter) and set its allocation. The reverse flow lives on the Project's Contracts tab — see §7 Projects.
- Create project — quick-create from this contract. Gets auto-linked.
The Unallocated card at the top nudges you: if there's still money not assigned to a project, the card gets a gold border.
What the backend enforces on every link (regardless of which side you started from): the project's service must be in the contract's services array; the new allocation + existing allocations can't exceed the contract value; the same (contract, project) pair can't be linked twice. Status of either side doesn't gate eligibility — a "Pitch - Active" project can be linked to a signed contract, an "On hold" project still receives receipts via its links.
Amendments tab
Change log. When a signed contract needs to be modified:
| Value revised | Change the number with a reason. No scope change. |
| Scope added | Bump the value because the client asked for more work in an existing service. |
| Service added | Started Arch-only, now adding Interior. Usually bumps the value too. |
| Service removed | Client dropped a service. This cascades — any project links under that service are automatically terminated. |
The Terminate button
Top-right of the contract detail page. For when a contract dies mid-way — client walked away, deal fell through, any "this contract is dead" situation.
Click it → dialog asks for a reason → confirm → status flips to terminated, all active project links cascade to terminated, everything gets logged. Nothing is deleted. The paper trail stays intact.
07 — Module deep dive
Projects.
A chunk of work TPA delivers. One service = one project.
The list view
Simple table: Project ID / Service / Name / Site / Start date / Status. Filters for status and service. Click any row to drill in.
Creating a new project
| Project ID | Optional. Leave blank and the system assigns PROV-NNN-ARCH. You can rename it later. |
| Service | Pick Architecture, Interior, or Municipal. Just one. |
| Project name | A friendly human name: "Mital Penthouse — Koregaon Park". |
| Client company | Free text. |
| Project type | From the Project Type dropdown master. |
| Site location | From the Site Location dropdown master. |
| City | Free text — Mumbai, Pune, Bengaluru, etc. |
| Description | Brief project scope or notes. Free-form textarea. |
| Area (Sq Ft) & Per Sq Ft Rate | Per-project metrics — moved here from the contract module in May 2026 because a contract may bundle multiple services across different areas. |
| Project value | The single source of truth for the project's money. See "Project Value & Alignment" below. |
| Contract expected by | If you're running without a contract yet, this is your nagging reminder date. |
| Expected close date | When the project is expected to wrap. |
| Status | From the Project Status dropdown master. |
| Project Owners | Multi-assign from the active employee list. Coordination/delivery label, distinct from commission roles. Renamed from "Project Leads" in May 2026 to disambiguate from sales leads. |
Click Create. The project exists but isn't linked to a contract yet — do that from the contract detail page.
The project detail page
Where the project's story lives. Four summary cards at the top:
| Service | The service badge (ARCH / INT / MUNI). |
| Status | Active / On Hold / Completed / etc. |
| Project Value | Total money from all active linked contracts. |
| Work Progress | The PM's self-reported % complete. Click it to update. |
The Work Progress card is clickable. Click it → a dialog opens where the PM can set the new percentage (0-100), add notes ("Mid-CDs, client review next Tuesday"), and see when it was last updated. Every update is audited.
Tabs: Overview · Details · Payment Schedule · Contracts · Payout · Audit
The KPI cards sit above the tabs. Each tab is its own surface:
- Overview — Financials card (Project Value / Contracts / Received / Pending), per-contract breakdown, and the alignment chip.
- Details — Area & Rate, General Info (Site location, City, Description, Expected close date), and Project Owners. Each section is its own Card with an Edit button; everything edits in place. New in May 2026 — these three blocks used to sprawl across the top of the page; they're now in one tab.
- Payment Schedule — milestone rows (see §8).
- Contracts — every contract linked to this project. Two buttons: Link existing contract (pick any contract, set allocation) and Create contract from this project (quick-create). The existing-link picker excludes contracts that are already linked.
- Payout — per-project commission configuration (see §11).
- Audit — append-only log of every change.
Project Value & Alignment
One number, set once, reads everywhere. Where "Project Value" comes from and what the alignment chip is telling you.
Project Value is the single source of truth for a project's money. It's the number the project is budgeted at — not the contract, not the payment schedule sum, not the allocation. Every downstream view (Overview financials, Payment Schedule %, Contract breakdown, Payout projections) derives from it.
Where the value comes from
| Entered directly | You type it on the project (create form or edit dialog). Most pre-signing projects start this way — you know the target fee before the contract is cut. |
| Seeded from a contract | When the first contract is linked to a project whose value is blank, the platform auto-fills it from the contract's allocated amount. You see this as a one-time fill — after that, the value is locked to what you entered. |
| Seed button (manual) | If a project was created before its contract existed and its value is still blank, a small Seed from allocation button appears on the alignment chip. One click pulls the allocated value across. Same effect as auto-seed, but explicit. |
The alignment chip
Sitting next to Project Value on the detail page is a small chip that compares Project Value against the sum of active contract allocations. Four states:
| ● Aligned | Project Value equals the sum of allocations. The happy path — nothing to do. |
| ▲ Over-contracted | Sum of allocations > Project Value. Usually means a contract was linked with a higher allocation than the project was sized for. Either bump the project value or reduce the allocation. |
| ▼ Under-contracted | Sum of allocations < Project Value. The contract(s) aren't yet covering the full project budget. Expected for projects mid-negotiation. |
| ○ No value set | Project Value is blank but contracts are linked. A Seed from allocation button appears — click it to pull the allocated total in. |
received_amount > 0. Only untouched (zero-received) rows recompute on a value change. This protects historical receipts from drift — a ledger entry from 3 months ago won't silently shift because someone bumped Project Value today.
The tabs below the cards
Overview
The Financials card is the centrepiece: three numbers (Value, Received, Pending) and a green progress bar showing % received.
Below those numbers, a chevron toggle labelled "By contract" — click to expand a mini-table showing each funding contract with its own Value / Received / Pending breakdown. Collapsed by default so the summary stays clean.
Below the Financials card, the Project details card: Project ID & Name, Location & Project Type, Start date & Contract expected by, plus any progress notes.
Payment Schedule
The milestone list. See the full walkthrough below.
Contracts
Every contract funding this project. Usually just one, but in the phased-funding case, two or more.
Payout
The per-project payout configuration — who on TPA gets a cut of this project's receipts, at what rate, for which date range, and with which receipts excluded.
Each person you add is an assignment (project × employee). Each assignment has one or more rate periods (date-effective). Any receipt can be excluded from an assignment with a reason. The Ledger view inside the tab shows the per-receipt projection — what each person would earn on every receipt — with exclusions, uncovered periods, and rate mismatches called out in gold.
Full walkthrough in the Payout module section.
Audit
The full change log for this project. Every edit, every status transition, every financial adjustment, every progress update. Stamped with who, when, and the before/after. No way to delete entries — that's the whole point.
08 — Module deep dive
Payment Schedule.
The milestone-level view of how we invoice a project.
Every project should have a payment schedule — a list of milestones with fees. This drives how much we invoice at each stage, what work the PM is tracking, and where money lands when the client pays.
The columns
| # | Sequence — the order milestones run in. |
| Milestone | The name (e.g. "Concept Design"). |
| Contract | Which contract this is billed against. |
| % | This milestone as a % of the contract value. |
| Fee (₹) | The amount in rupees. |
| Completion % | Read-only on this table — but click-through. The value mirrors what PMs enter in the Project Tracker; clicking the cell jumps you directly to the Tracker with that project auto-expanded and a "Back to project" link at the top. Finance reads here, PMs edit there. |
| Received (₹) | How much money has actually come in against this milestone. |
| Pending (₹) | New in May 2026. Fee − Received, gold-coloured when > 0, "—" when fully paid or zero-fee. |
| (actions) | Hover over a row → a small ✕ appears on the right. Click to delete. |
Rollup strip above the table: Schedule total % · Received · Pending · % Billed. At a glance: how much of the schedule is laid out, how much money has actually arrived, what's still outstanding.
Adding a milestone
- Click "Add item" in the top right.
- Pick a contract from the dropdown. A helper banner appears:
Contract X · Value ₹50L · Allocated ₹15L · Remaining ₹35L. - Type the milestone name like "Concept Design".
- Type either % or Fee — the other fills in automatically based on the contract value.
- Click ✓ to save, or ✕ to cancel.
Guardrails
You cannot enter a fee bigger than the Remaining value. The fee cell turns red and the save button disables. The backend also double-checks this on every request — so there's no way to over-allocate a contract even if someone tries to bypass the form.
Deleting a milestone
Hover over a row. A small ✕ appears on the right. Click it, confirm, done.
Exception: you cannot delete a milestone that has already received money. First the receipt needs to be voided (handled by the Receivables module — currently a rough edge).
Recording a receipt
Click Record receipt next to "Add item". A dialog opens.
| Amount | How much money came in. |
| Contract | Only shown if the project has more than one linked contract. Otherwise auto-picked. |
Click Apply. The amount waterfalls from the top:
Say the schedule has four items: [₹2L, ₹3L, ₹5L, ₹5L] and current received amounts are [₹2L, ₹1L, 0, 0]. A new ₹4L receipt arrives:
- Milestone 1: already full → skip
- Milestone 2: ₹2L capacity left → take ₹2L → now full at ₹3L
- Milestone 3: ₹5L capacity → take ₹2L → now at ₹2L/₹5L
- Milestone 4: untouched. Receipt exhausted.
Why waterfall? Payment schedules are typically ordered by completion (Concept → Schematic → DDs → CDs). Money usually arrives in that order. Filling top-down matches reality.
09 — Module deep dive
Status Updates.
The PM's home page. Progress tracking only — zero financial data.
Status Updates is a dedicated page for Project Managers who need to update project progress but shouldn't be seeing money, payout config, or contract values.
What you see
A list of projects you can update:
| Project ID | The code. |
| Project name | The friendly name. |
| Client / Location | Who it's for and where. |
| Service | ARCH / INT / MUNI badge. |
| Progress | A mini progress bar with the current %. |
| Last updated | When the PM last moved the needle. |
| Update | A button to open the update dialog. |
A search box at the top for quick filtering, and a status filter (default: Active only).
Updating progress
Click Update on any row. A dialog asks for:
- The new progress % (0-100)
- Notes — optional, free text ("Schematic phase, client review next Tuesday")
Click Save. Done. The update writes to the audit log.
Three doors land here
New in May 2026. Updating completion is the same job no matter where you came from — so the tracker accepts you from three other pages with a "Back to..." link at the top:
- From Project → Payment Schedule tab: click any milestone's Completion % cell.
- From Meetings: in a session, expand a project block and click Update progress.
- From Projections: click any row's % Complete cell.
All three land at /status-updates?project=<id> — the project's card is auto-expanded and scrolled into view, the status filter defaults to "All statuses" (so on-hold / pitch / lead projects still show up), and the top of the page has a back-link to where you came from.
09a — Module deep dive
Projections.
"What's coming in" — a billing-prioritization view, not a sales pipeline.
Projections at /projections answers one question: what raises are coming, when, with what confidence, and how does that map to outstanding pending billing? A project shows up here when an associate has captured a "we can raise X targeting month Y" entry — either in a meeting or directly from this page (the "+ Add raise" button).
/leads. Projections is purely about money coming in from active project work.
Cards above the table
Three KPI cards summarize the active filter window (default: next 6 months):
- Weighted Pipeline — confidence-adjusted total (Confirmed × 100% + Likely × 70% + Uncertain × 30%).
- Confirmed — raises tagged Confirmed.
- Likely — raises tagged Likely.
A "Scope" line under the header tells you the active month range and confirms the cards are the sum of the table below.
Filters
| Project Owner | Scope to one owner's raises (employees who own at least one project that has raise data). |
| Time horizon | Next 3 / 6 / 12 months or All months. |
| Show dismissed | Off by default. Toggle on to see realized/dismissed raises (greyed out + restorable). |
The Project-wise Raises table
Sorted by target month ASC → confidence DESC (Confirmed first) → weighted amount DESC. Most actionable raises at the top.
| Project | Name + project code. Clicking lands on the project detail page. A small Likely realized badge appears in gold when receipts on/after the target month already cover the raise — a soft hint to consider dismissing. |
| % Complete | Project-level overall progress (from the Tracker). Click-through to the Tracker. |
| Billed | What percentage of project value has been received. Tooltip: ₹X of ₹Y. |
| Pending | Project value − received, in gold. |
| Raise (target) | The raise amount + target month. |
| Confidence | Editable inline. Drop a Confirmed → Uncertain and the KPI cards above recompute immediately. Every flip is audited. |
| Weighted | Raise × confidence weight. |
| Dismiss / Restore | Mark a raise as "realized / no longer applies". Dismissed rows hide by default; restore from the "Show dismissed" toggle. |
Click a row → milestone breakdown
Each row is clickable (except the interactive cells). Expansion shows a sub-table per project with all milestones: # / Milestone / % / Fee / Compl% / Received / Pending, plus a Totals row. This is the "how much is left to bill" view at granular detail.
"+ Add raise" inline
Top-right of the page. Captures a raise without going through a meeting: pick project, raise amount, target month, confidence. Saves to a synthetic "Projections — direct entries" meeting so the rest of the system reads it the same as a meeting-captured raise.
If the project already has a raise on file (in any month), the dialog surfaces a warning with the existing row. Clicking Overwrite existing raise updates that row in place (audited as meeting.note.inline_raise_overwrite) rather than creating a second one.
Confidence weights
| Confirmed | 100% — invoice raise is committed (signed-off scope, partner agreement). |
| Likely | 70% — high likelihood but not yet committed. |
| Uncertain | 30% — speculative. |
10 — Module deep dive
Payout.
Who on TPA gets a cut of a project's receipts, at what rate, for which date range. Lives on the project detail page under the Payout tab.
Payout is the per-project compensation config. It answers a deceptively simple question: when a receipt of ₹X lands on this project, who gets paid and how much?. The old "one number per project" approach didn't survive contact with reality — partners negotiate custom rates, rates change mid-project after appraisals, specific receipts get carved out for specific people. So the module is built around three concepts that stack cleanly.
The three concepts
| Assignment | One row per (project, employee) pair. "Shefali is on NM-C-TOWER." Each assignment has a category inherited from the employee master — pnl, mgp, variable, or normal — and a denormalized rate that mirrors the currently-open rate period for fast list reads. |
| Rate period | A date-effective rate inside an assignment. "Shefali earns 15% from 1 Jan to 28 Feb (closed), and 17% from 1 Mar onwards (open)." Appraisal bumps, promotions, negotiated rate changes — all live here. The truth is the rate periods; the assignment row is just a snapshot of the latest one. |
| Receipt exclusion | "Don't apply Shefali's rate to receipt #14 — that was a finder's fee carve-out." Excluding a receipt requires a non-blank reason. Excluded receipts show up on the Ledger view with the reason displayed, so the exclusion is visible forever. |
The four categories (set on the Employee master)
Each employee has exactly one payout_category (or blank). The category determines how the Ledger treats their receipts.
| PNL | Flag-only — no per-receipt rate. Used for partners who take a profit share on projects they own. The Payout module records the assignment; the actual profit-share settlement happens outside the ledger. There is no rate field to edit. |
| MGP | Minimum-guarantee employee. Rate is compared against employees.default_commission_pct. If you enter a different rate, the platform marks it as source='override' and a reason is required. The override + reason both show on the Ledger, so the deviation is never silent. |
| Variable | Ad-hoc bonuses. Manual rate entry. No comparison against any house rate. The rate is whatever you type, for whatever period you type it for. |
| Normal / blank | Vanilla commission via contract roles. The rate comes from contract_roles.rate_pct on the contract the receipt lands on, falling back to employees.default_commission_pct. The Payout module still records these people, but the rate source is the contract role — not a per-project override. |
commission_ledger_entries) still reads rate from contract_roles.rate_pct → employees.default_commission_pct. Reconciling the two is a deliberate future decision; for now, treat Payout as the richer "what should happen" view and Ledger as the "what actually got posted" view.
Working with the Payout tab
Adding an assignment
Click Add person. Pick the employee. The dialog reads their category from the employee master and shows only the fields that apply:
- PNL — just the employee. No rate field.
- MGP — employee + rate. If your rate ≠ their default commission %, a remark field becomes required.
- Variable — employee + rate. No comparison, no forced reason.
- Normal — employee only. Rate comes from contract roles at ledger time.
Save. The assignment appears with its first rate period starting on your effective from date and no end date (open).
Changing a rate (appraisal, promotion, negotiated bump)
Click the [+] icon next to an existing rate period. The Add period dialog asks for the new rate, the new effective_from, and a reason. Saving auto-closes the prior open period one day before your new start date, then inserts the new period as the currently-open one. Historical rates are preserved — old receipts never silently re-rate.
effective_from = 1 Mar, rate 17%. The prior period auto-closes on 28 Feb. Receipts in Jan and Feb still project at 15%; receipts from March onwards project at 17%.
Editing a period in place
Click the pencil icon on the period row — a fat-finger typo in the rate, or a wrong effective-from date. This edits the period without creating a new one. The audit log captures the before/after. Cannot delete the only remaining period on an assignment (delete the assignment instead).
Excluding a receipt
Find the receipt in the Ledger view (below the assignment list). Click the small ✕ on the row. A dialog asks for the exclusion reason — it's non-optional. Save. The receipt now shows excluded: true, applied_pct: null, due: 0 with the reason visible in the cell.
The Ledger view (inside the Payout tab)
Below the assignment list, the Ledger shows what each person would earn on every receipt. A few rules to read it correctly:
| Per-person totals | receipt_total is per-(person, project) — sum of NOT-excluded receipts for that specific assignment. It is not project-wide. |
| Strict backward period match | If a receipt lands on a date no rate period covers (e.g., after the latest closed period, before the next one starts), applied_pct is null, the row shows "⚠ Not mapped" in gold, and the parent assignment row gets a gold ⚠ predictive trigger. This is a feature — it surfaces period coverage gaps. |
| Excluded receipts | Excluded rows appear with the reason, don't contribute to receipt_total, and have due = 0. |
| Uncovered receipts | Still count toward receipt_total but contribute 0 to due, giving you a deliberate "Receipt × Rate ≠ Due" mismatch. The mismatch is the signal that coverage needs attention. |
| Edit lives at the child level | The parent assignment row has only [+] (new period) and [✕] (remove assignment) buttons. All edits happen on the period row itself. This is deliberate — stops "what rate is currently active?" from drifting between the parent snapshot and the child periods. |
Draft review & publish (admin-only)
Payout assignments can be drafted by anyone with payout.create but remain in a draft state until reviewed. Reviewing and publishing drafts requires admin or super_admin role — not just a permission. This is wired as an RBAC-adjacent guard: if your role isn't admin or super_admin, you can create and edit drafts but cannot move them live.
/projects/NM-C-TOWER?tab=payout. It has the Shefali appraisal (15% closed → 17% open), live exclusions, and the gold predictive trigger visible in the list view.
11 — Module deep dive
Commission Ledger.
The append-only record of every rupee of commission the firm has ever recognised. The abuse-killer.
Commission Ledger is the money-fact layer. The Payout module says "what should happen" — this module is "what actually got posted". Every row comes from a receipt landing on a project deliverable, with a fully-logged chain: source receipt → contract → applied rate → rate source → entry. There is no way to edit a row. Corrections are new offsetting rows that reference the original via reverses_entry_id.
commission_ledger_entries table has a database trigger that raises on UPDATE and DELETE. Even if application code has a bug, the database refuses to mutate a historical entry. This is non-negotiable — it's the property that makes the ledger trustworthy for payout disputes.
The list view — /commission/ledger
Every entry, with filters by employee, period, project, receipt. Columns: Entry # · Employee · Project · Contract · Receipt date · Receipt ₹ · Applied % · Due ₹ · Status (active / reversed / reversing).
The drill-down — /commission/ledger/employees/{id} (the abuse killer)
The single most important screen in the platform. For one employee, every credit they've ever received, with:
- Source receipt — which payment came in when, from which client.
- Contract — which contract the receipt landed on.
- Applied rate — the exact percentage used.
- Policy explainer JSON — a small modal showing the full reasoning:
source(manual_role_rate= came fromcontract_roles.rate_pct, oremployee_default= fell back toemployees.default_commission_pct), the contract_role row that was read (if any), and the resolved final rate.
If someone disputes their commission, this screen is the answer. Open it, show them the chain, close the conversation.
How entries get created
There is no manual-create form. Ledger entries are produced by the receipt-mapping flow:
- A receipt comes in, gets mapped to a project deliverable via Receivables.
- For each commission role on the underlying contract (AP, PA, Finder, Converter), the ledger service resolves a rate using the precedence rule below.
- One ledger entry per role-holder is inserted in the same DB transaction as the receipt map. The audit row goes in the same transaction too. Atomic, or nothing.
Rate source precedence
contract_roles.rate_pct— per-contract override on a role. Percentage scalar (15.0 = 15%). Source label:manual_role_rate.employees.default_commission_pct— the employee's house rate. Fraction (0.1500 = 15%); the ledger multiplies by 100 internally. Source label:employee_default.
No policy table, no lookup — just these two sources. The precedence is deliberate: per-contract overrides win, falling back to the employee default. Every entry records which source it used.
Closing a period
Go to Commission Ledger → Close period. Pick the period (month, quarter). The close action is approval-gated — requires the commission.ledger.close_period permission and is one of the 7 approval target types. Once approved, entries within the period are locked: they still appear in the drill-down forever, but no new entries can be inserted into a closed period.
Corrections (new offsetting rows)
If a historical entry is wrong, use Correct entry. The platform proposes an approval request of type ledger_correction with the offsetting payload. On approval, two new rows go in: the reversing row (negative ₹, reverses_entry_id set), and the corrected row. The original entry is untouched. The drill-down shows all three, with the chain visible.
12 — Module deep dive
Receivables.
The list of every receipt from a client, with its mapping to the project deliverable it covered.
Go to Receivables. Every payment that came in — across every project, every contract — in one list. Filters for date range, client, project, and mapping status (mapped / unmapped / partially mapped).
Columns
| Receipt ID | Auto-generated. Sequential. |
| Date | When the money landed. |
| Amount (₹) | What came in. |
| Payer | The client entity that paid. Can be a company name, a proprietor, anyone. |
| Instrument | NEFT / RTGS / UPI / Cheque / Cash. Used for reconciliation with bank statements. |
| Mapped to | Which project deliverable(s) this receipt's money hit. Can be one or many if the waterfall split a single receipt. |
| Status | Mapped / Unmapped / Partially Mapped / Voided. |
Recording a receipt
Two entry points:
- From the project detail — Payment Schedule tab → Record receipt. The contract is auto-scoped to the project; the waterfall applies across the project's deliverables.
- From Receivables itself — New receipt. Lets you record a receipt that hasn't been mapped yet (e.g. an advance before the project is created). The Map button on the row becomes your next step.
Auto-suggest mapping
When you map an unmapped receipt, the platform suggests which project it most likely belongs to based on: payer name match, amount match to an expected deliverable, and recent activity on the project. Accept the suggestion with one click, or pick a different project manually.
Voiding a receipt
If a receipt was entered wrong or the underlying payment bounced, use Void. This is approval-gated — type receipt_void, requires the receipt.update permission plus an approval flow. On approval, the receipt flips to Voided, its mapped amounts are rolled back from every deliverable it hit, and any commission ledger entries it produced are auto-reversed. Nothing is deleted; everything stays visible with the void stamp.
Remapping a receipt
If a receipt was mapped to the wrong project, use Remap. Also approval-gated (receipt_remap). The flow rolls back the old mapping and applies the new one atomically. Commission ledger follows along.
13 — Module deep dive
Approvals.
The pending queue — things proposed that need someone else's sign-off before they go live.
Some actions on the platform are too consequential for one person to do unilaterally. These go through an approval flow: someone proposes, one or more approvers decide, and only on approval does the actual write happen. The proposer cannot approve their own request.
The seven target types
noop | A test target used for wiring and drills. Approving does nothing. |
contract | Creating a contract that needs sign-off (rare — most creates don't gate). |
contract_amendment | Every amendment (Value Revised / Scope Added / Service Added / Service Removed) goes through here. |
receipt_remap | Moving a receipt from one project to another. |
receipt_void | Voiding a receipt. |
ledger_correction | Posting a correcting pair in the commission ledger. "All" semantics — every listed approver must sign off. |
period_close | Closing a commission period. "All" semantics. |
Approval semantics
- Any — one approval is enough. Used for most mutations.
- All — every required approver must sign off independently. Used for commission policy, ledger corrections, period close. Rejection by any one kills the request.
The list view — /approvals
Everything pending, split into two tabs:
- For me — requests waiting on your decision.
- By me — requests you proposed that are still pending (or were rejected).
Click any row for the detail view.
The decide dialog
Opens when you click Act on a row. Shows:
- The proposer, the request time, and the target type.
- The payload — the exact change that will be applied if approved (JSON-formatted for full transparency).
- Other approvers on this request (if "all" semantics) and who has already decided.
- Approve / Reject buttons. Rejecting requires a reason.
What happens on approval
When the threshold is met (any or all), the platform looks up the registered applier for the target type. The applier replays the payload through the originating service, which writes the real row and its own audit row in the same DB transaction. If the applier fails (e.g. a uniqueness violation because something changed between propose and approve), the approval stays pending and the error surfaces — nothing partial gets written.
14 — Module deep dive
Meetings.
Review meetings, agendas, and action items with owners.
Meetings is where the firm tracks the working rhythms it used to run out of WhatsApp and spreadsheets. Weekly PM reviews, monthly finance reviews, project check-ins. Each meeting has an agenda, notes, and action items; each action item has an owner and a due date.
The list view
Every meeting, filterable by date range, type, and attendee. Each row shows date, title, attendees, and open action item count.
Creating a meeting
Click New meeting. The form asks for:
- Title, date, type (review / check-in / standup / ad-hoc)
- Attendees — pick from the employee master (multi-select)
- Linked project (optional) — if the meeting is about one specific project, link it so it shows up in that project's context
Inside a meeting
Two main tabs:
Notes
Free-form markdown area. Write discussion notes, decisions, context. Everyone who was an attendee can edit.
Action items
A list of concrete TODOs generated from the meeting. Each has:
| Description | What needs to happen. |
| Owner | Who's responsible. Picked from the employee master. |
| Due date | By when. |
| Status | Open / In Progress / Done / Cancelled. |
| Linked entity | Optional — the project, contract, or employee this item is about. Click-through goes to that entity. |
Action items roll up to the owner's dashboard: every person sees their open items across every meeting in one place.
15 — Module deep dive
Employees.
The canonical employee master. One row per person. Everything about a person lives here.
Go to Employees. The list view is a searchable, filterable table — filter by home offering, payout category, active / terminated, and whether they have platform access.
Employee detail page
Clicking a row opens the detail page with several tabs:
Profile
The fields from the master table (see One-time setup). All editable inline, audit-logged, with save-in-card.
Roles on contracts
Every commission role this person currently holds or has held — AP, PA, Finder, Converter, per contract, with dates and rate. Read-only here; the canonical source is the contract's Roles tab.
Payout assignments
Every per-project payout assignment this person has, grouped by project, with the current rate period. Click through to the project's Payout tab.
Commission history
Direct link to the Commission Ledger drill-down — the abuse-killer view for this employee.
Platform access
See the Admin section for the provisioning flow. From the employee detail page, the Platform Access card shows the current state (enabled / not enabled), the provisioned email, the assigned role, and buttons to revoke or change role.
Commission roles & the cross-offering rule
The home offering field is load-bearing. When you try to add someone as AP or PA on a contract whose offering doesn't match their home offering, the platform rejects it with a clean error: "Carl (home: Architecture) cannot hold Principal Associate on an Interior contract." Finder and Converter roles are not constrained — anyone can bring in a lead or close one. Honorary employees cannot hold any commission role regardless.
16 — Module deep dive
Admin.
RBAC, platform access, integrations, email. The settings area for super admins and whoever holds rbac.write.
Go to Admin. Five sub-areas:
RBAC — roles & permissions
TPA Ops ships with a permission model of 52 permissions across 13 modules, following the module.action convention (e.g. project.create, receipt.update, employee.delete, commission.ledger.close_period).
Roles list (left sidebar)
System roles (super_admin, admin, finance, pm, associate, viewer, etc.) plus any custom roles you've added. System roles can't be deleted, but their permissions can be edited.
Permission matrix (detail pane)
Checkbox grid — 13 module groups × create/read/update/delete + specials (contract.amend, receipt.map, commission.ledger.close_period, approval.act, export.create). A module-level toggle at the top of each group checks/unchecks everything in that module in one click. Changes auto-save after a 600ms debounce — no Save button.
Assigned users (below the matrix)
Everyone who currently has this role, with the grant-date and who-granted-it. Remove or reassign inline.
Platform access provisioning
User accounts are admin-provisioned, not self-registered. The flow:
- Make sure the employee is in the Employee master with an email on the row.
- Go to Employees → open the person → Platform Access card → Enable access.
- Pick their role (usually from the role list you've built on the RBAC tab).
- Click Provision. The platform calls Supabase's Admin API, creates an auth user with a temp password, creates the local
usersrow, links it to the employee viaemployee.user_id, and assigns the role. - The temp password is shown once — copy it, send it securely, gone.
Revoking access
Same card → Revoke. This bans the Supabase auth user (100-year ban, reversible), deactivates the local user row, and unlinks the employee's user_id. The employee row itself stays — only platform login is gone.
Resetting a password (admin-driven)
If a user can't access their email or is locked out, an admin can reset their password from the same Platform Access card → Reset Password button. Two prompts: optionally type a custom temp password (leave blank for the default TpaOps@<e_code>), then confirm. The new temp password shows in a 15-second toast — copy it and send via secure channel. The change is audit-logged with action employee.reset_password.
For routine forgot-password cases (user has email access), prefer the user-driven Forgot link on the login page — no admin involvement needed, and the user picks their own value.
Changing someone's role
From the Platform Access card, pick a different role. Effective immediately; their next request hits the new permission set.
Dashboard Access — assigning the dashboard tier
Each platform user has a dashboard tier that controls which dashboard they land on after login. There are 7 tiers: partner, admin, pnl, mgp, finance, hr, finance_hr, plus none (generic landing). Each renders a tier-specific dashboard component.
Admin → Dashboard Access lists every platform user with their current tier. Pick a tier from the dropdown to change it; auto-saves. Tier assignment is fully admin-managed — never auto-derived from role or permissions.
One carve-out: super_admin users with tier set to none are auto-promoted to the admin dashboard at read-time. So you don't have to set a tier explicitly for super_admins — they'll see the operational command centre by default.
Tier preview (super_admin only)
Add ?preview=<tier> to any URL when logged in as super_admin to render the dashboard as if you're in that tier — without changing the DB. Useful for inspecting all 7 tier views without re-provisioning users. Examples: /?preview=partner, /?preview=mgp, /?preview=finance.
Integrations
Configuration for external systems the platform talks to. At launch, this mostly means the email sender (SMTP credentials for the outbound email queue) and any future webhook endpoints. Credentials are stored encrypted.
Email configuration
The outbound email queue uses ARQ (async job queue). Admin → Email lets you:
- Pick the active sender:
stub(logs only, dev use),resend(production — API-key based),outlook(production — OAuth-based). - Send a test email to verify the configuration before flipping the sender live.
- See the recent send log — last 50 emails with status, timestamp, and any failure reason.
Dropdown Master
Same page as Dropdown Master — lives under Admin on the sidebar but has its own dedicated page for frequent use.
17 — Module deep dive
Dropdown Master.
Where the lists that drive everything on the platform live.
The short version: this is where status labels, city names, project types, lead sources, and project statuses live. Everyone on the platform uses these lists.
Add a new status or city once, and it appears in every dropdown, every filter, every detail page, every report automatically. No code change. No developer needed.
See One-time setup for the fuller story on each category.
18 — Deeper patterns
The 3 shapes of a deal.
TPA's deals aren't always a simple one-contract-one-project arrangement. The platform supports three shapes — one simple, one common, one that confused everyone in the old Excel world.
The most common case. Single service, single project, one-for-one relationship.
When it happens: Straightforward Arch-only or Interior-only engagement. What to do: Create the contract, create the project, link them with 100% of the contract value allocated. Everything else just works.
One legal document covers multiple services, each of which becomes its own project.
ARCH + INT
When it happens: Client signs one deal that covers both Architecture and Interior work. What to do: Create the contract with both services. Create two projects (one per service). Link both projects to the contract with allocations that sum to the contract value.
The interesting one. Multiple contracts fund the same physical project — usually in phases over time.
Closed
Active
Total ₹40L
When it happens: A long-running project (like a tower) is signed in phases. Phase 1 covers concept + schematic; Phase 2 covers DD + CA. Same building, same team, two separate contracts. What to do: Create the project once. Create both contracts. Link both contracts to the project. Each contract gets its own payment schedule. When money comes in, the Record Receipt dialog asks which contract it's for.
Every Project Value state — the 20-scenario reference
Every combination of Project Value + contracts + receipts you can hit, and what the platform will show you.
The three shapes above cover structure (how contracts and projects relate). The table below covers state — what happens at every combination of "Project Value typed or not", "contracts linked or not", "money received or not". Use it as a lookup: find the row that matches the project in front of you, read across to see what the platform should be showing.
docs/project-value-alignment-scenarios.pdf — hand that one to anyone who wants the deep-dive version.
| # | Situation | Value | Contracts | Chip | On screen | Do this |
|---|---|---|---|---|---|---|
| 1 | Brand new — lead just converted, nothing signed | Not set | 0 | hidden | All zeros · "none linked yet" | Click the Value card, type estimated budget |
| 2 | Estimated budget — pre-signing | ₹5 Cr | 0 | hidden | Pending ₹5 Cr · no contracts yet | Wait for contract, link when it arrives |
| 3 | Advance before contract — retainer/goodwill | ₹5 Cr | 0 | hidden | Received ₹50 L · Pending ₹4.5 Cr | Backend supports it; UI flow pending |
| 4 | Contract signed, matches estimate | ₹5 Cr | 1 · ₹5 Cr | ● Aligned · ₹5 Cr | Value = Σ · Pending ₹5 Cr | Start invoicing, add deliverables |
| 5 | Contract came in higher than estimate | ₹5 Cr | 1 · ₹6 Cr | ▲ Over by ₹1 Cr | Pending still from ₹5 Cr | Bump value to ₹6 Cr, or confirm scope TBD |
| 6 | Contract came in lower than estimate | ₹5 Cr | 1 · ₹4 Cr | ▼ Under by ₹1 Cr | Pending from ₹5 Cr | Lower value, or expect amendment |
| 7 | Contract linked, Project Value never typed (Godawari case) | Not set | 1 · ₹1.6 Cr | ○ No value set · [Seed] | Pending falls back to ₹1.6 Cr | Click Seed → value becomes ₹1.6 Cr |
| 8 | Phase 1 + Phase 2 — two contracts on one project | ₹10 Cr | 2 · ₹6 + ₹4 | ● Aligned | "By contract" expando shows both | Treat as one project |
| 9 | Over-committed multi-contract | ₹10 Cr | 3 · ₹5+₹4+₹3 | ▲ Over by ₹2 Cr | Σ ₹12 Cr in breakdown | Raise value or reallocate a slice |
| 10 | Bundled contract — one contract spans many projects | ₹2 Cr | 1 slice · ₹2 Cr | ● Aligned · ₹2 Cr | Only this project's slice is shown | Nothing. Siblings on contract page |
| 11 | First invoice raised and received | ₹5 Cr | 1 · ₹5 Cr | ● Aligned | Received ₹75 L · Pending ₹4.25 Cr · bar 15% | That row freezes — won't recompute on value edit |
| 12 | Scope change — edit Project Value mid-project | ₹5→₹6 Cr | 1 · ₹5 Cr | ▼ Under by ₹1 Cr | Un-received rows scale up; received rows stay | Amend contract to match the new value |
| 13 | Fully received — project paid in full | ₹5 Cr | 1 · ₹5 Cr | ● Aligned | Received ₹5 Cr · Pending ₹0 · bar 100% | Mark project Completed |
| 14 | Contract terminated mid-project | ₹5 Cr | 0 active (1 terminated) | ▼ Under by ₹5 Cr | Pending ₹3 Cr (value − received) | Close project, or wait for replacement |
| 15 | Contract amended — value revised upward | ₹5 Cr | 1 · was ₹5, now ₹6 Cr | ▲ Over by ₹1 Cr | Σ contracted now ₹6 Cr | Bump Project Value to match |
| 16 | Seed clicked — row 7 resolved | ₹1.6 Cr | 1 · ₹1.6 Cr | ● Aligned | Value = Σ · audit records seed event | Ready to invoice |
| 17 | Project Value cleared back to empty | Not set | 1 · ₹5 Cr | ○ No value set · [Seed] | Pending falls back to Σ | Re-seed or type a new value |
| 18 | Zero-value project (explicit ₹0) | ₹0 | any | depends on Σ | Pending always ₹0 | Edge case — likely data entry error |
| 19 | All contracts terminated | ₹5 Cr | 0 active (2 terminated) | hidden or under | Active contracts: 0 | Same as #14 |
| 20 | Orphan contract — signed but not linked | no project-page involvement | Flagged "unallocated" on contract page | Link it to a project | ||
Scenario detail — one block per row.
Plain English walkthrough of each scenario from the matrix above. If the behaviour you see doesn't match the On screen line, take a screenshot — it's a bug.
Situation. You just converted a lead into a project. No value typed, no contract linked. It's a placeholder.
On screen. Project Value card says "Not set". Alignment chip is hidden (nothing to compare). Overview Financials: all zeros, with "none linked yet" under Contracts.
Action. Click the Project Value card → type the estimated budget you'd quote the client. That's enough to move on — the contract can follow later.
Situation. You've typed a budget (say ₹5 Cr) but no contract has been signed yet. Common when the client is still in discussion.
On screen. Value: ₹5 Cr. Contracts: 0. Chip: hidden (nothing to compare against yet). Pending = ₹5 Cr.
Action. Nothing to do on this screen. Wait for the contract — link it when it arrives.
Situation. Client sent ₹50 L as a retainer before the contract was signed. Value is estimated at ₹5 Cr, no contract yet.
On screen. Value: ₹5 Cr. Contracts: 0. Received: ₹50 L. Pending: ₹4.5 Cr.
Action. Backend allows receipts without a contract. The UI flow for recording this receipt is being built — for now, finance records it and the value rolls up correctly.
Situation. Contract is in for ₹5 Cr, matches the ₹5 Cr you had estimated. The happy path.
On screen. Value: ₹5 Cr. Contracts: 1 · Σ ₹5 Cr. Chip: Aligned · ₹5 Cr. Pending: ₹5 Cr.
Action. Start invoicing. Add deliverables to the payment schedule.
Situation. You estimated ₹5 Cr. Contract is signed for ₹6 Cr (scope expanded during negotiation).
On screen. Value: ₹5 Cr. Σ contracted: ₹6 Cr. Chip: Over-contracted · +₹1 Cr.
Action. Edit Project Value up to ₹6 Cr so the value reflects what was actually signed. If the extra ₹1 Cr is a "scope to be confirmed" buffer, leave it — the chip is a flag, not an error.
Situation. You estimated ₹5 Cr. Contract came in at ₹4 Cr (client scoped down, or a second contract is expected later).
On screen. Value: ₹5 Cr. Σ contracted: ₹4 Cr. Chip: Under-contracted · −₹1 Cr.
Action. Two options: (a) lower the value to ₹4 Cr if that's the final deal, or (b) leave it — an amendment/second contract is expected.
Situation. Project was created earlier with a blank value. Now a contract has been linked for ₹1.6 Cr.
On screen. Value: "Not set". Chip: No project value set with an underlined Seed link. Pending falls back to ₹1.6 Cr (the contract value) as a best guess.
Action. Click Seed. One click — Project Value becomes ₹1.6 Cr, chip flips to Aligned. You're done.
Situation. A tower signed in phases: Phase 1 ₹6 Cr, Phase 2 ₹4 Cr. Project value ₹10 Cr.
On screen. Value: ₹10 Cr. Contracts: 2 (₹6 + ₹4). Chip: Aligned. Overview "By contract" breakdown shows both rows.
Action. Nothing special. Treat it like one project. Receipts get routed to the right contract via the Record Receipt dialog.
Situation. Three contracts linked (₹5 + ₹4 + ₹3 = ₹12 Cr) but Project Value is still ₹10 Cr — someone forgot to bump it after the third contract.
On screen. Value: ₹10 Cr. Σ contracted: ₹12 Cr. Chip: Over · +₹2 Cr.
Action. Bump value to ₹12 Cr, or reallocate one of the contract slices down.
Situation. Contract worth ₹5 Cr funds three sibling projects (Arch, Interior, Municipal). This project's slice is ₹2 Cr.
On screen. Value: ₹2 Cr. Σ contracted: ₹2 Cr (the slice, NOT the full contract). Chip: Aligned · ₹2 Cr.
Action. Nothing. Each project only sees its own slice. The contract detail page shows the full ₹5 Cr and who else is on it.
Situation. Contract ₹5 Cr, value ₹5 Cr, first milestone (₹75 L) has been received.
On screen. Value: ₹5 Cr. Chip: Aligned. Received: ₹75 L. Pending: ₹4.25 Cr. Progress bar at 15%.
Action. That ₹75 L row is now frozen — its fee won't recompute even if Project Value is edited later. This protects historical receipts from drift.
Situation. Project was ₹5 Cr. Client added scope. You edit value to ₹6 Cr. One milestone (₹75 L) has already received money.
On screen. Value: ₹6 Cr. Un-received milestones scale up proportionally (their percentages are preserved, fees recompute). The ₹75 L received row stays frozen at ₹75 L.
Action. Update the contract (amendment) to match the new ₹6 Cr value eventually — otherwise the chip flips to Under-contracted.
Situation. All milestones received. Nothing outstanding.
On screen. Received: ₹5 Cr. Pending: ₹0. Progress bar at 100%. Chip: Aligned.
Action. Mark project Completed (status dropdown on the project). The audit trail stays.
Situation. Contract was ₹5 Cr, ₹2 Cr received, then terminated (client walked, or scope killed).
On screen. Value: ₹5 Cr. Active contracts: 0 (one terminated). Chip: Under by ₹5 Cr (value is still ₹5 Cr but nothing is actively contracted). Pending: ₹3 Cr (value − received).
Action. Decide: close the project (lower value to ₹2 Cr to match received, mark Completed), or wait for a replacement contract to arrive.
Situation. Contract was ₹5 Cr. Amendment raised it to ₹6 Cr. Project value still at ₹5 Cr.
On screen. Value: ₹5 Cr. Σ contracted: ₹6 Cr. Chip: Over by ₹1 Cr.
Action. Bump Project Value to ₹6 Cr to match the amendment.
Situation. Godawari-style: was scenario 7 (no value, contract linked). You clicked Seed.
On screen. Value: ₹1.6 Cr (auto-filled from allocation). Chip: Aligned. Audit log shows a "seed from allocation" event.
Action. Nothing — project is ready to invoice.
Situation. Someone edited the Project Value field and wiped it (data cleanup, typo, whatever). Contract is still linked.
On screen. Value: "Not set". Chip: No project value set + Seed button reappears.
Action. Click Seed to restore from allocation, or type the correct value.
Situation. Someone typed 0 as the project value. Almost always a data-entry slip.
On screen. Value: ₹0. Pending: ₹0 regardless of contracts. Chip: depends on Σ (if any contracts, it'll show Over).
Action. Edge case — fix the value. A genuine zero-revenue project is rare; confirm before saving.
Situation. Project had two contracts, both terminated. Some money may already be received.
On screen. Value: ₹5 Cr (unchanged). Active contracts: 0 (2 terminated). Chip: hidden or Under.
Action. Same as scenario 14 — close out or wait for a replacement.
Situation. A contract exists in the system but isn't linked to any project yet.
On screen. Not visible on any project page. On the contract detail page, the "Allocated to projects" total is ₹0 and a flag reads "unallocated".
Action. Open the contract, click "Link existing" or "Create from contract" to attach it to one or more projects.
project.value is what YOU say the project is worth. Σ active contracted is what's legally signed. The chip reports their relationship. Σ received is money-in, flowing through deliverables. These three numbers stay honest to different sources of truth — the UI just shows you when they disagree so you can decide which to fix.
19 — When things happen
Common situations.
Real scenarios from day-to-day work, with step-by-step answers.
"The client walked away."
Go to the contract → click Terminate → give a reason. The contract flips to terminated, all project links cascade. Nothing is deleted — everything stays visible with a terminated badge for the audit trail.
"The client wants more work — add it to the contract."
Go to the contract → Amendments tab → pick the type:
- More money, same scope → Value Revised
- Added scope in an existing service → Scope Added
- A whole new service was added (was Arch-only, now also Interior) → Service Added
- Dropping a service → Service Removed (this cascade-terminates affected project links)
Every amendment writes to the change log with before/after snapshots.
"I started a project before the contract was signed."
Totally fine. Create the project anyway. In the Contract expected by field, set your reminder date. The project will have an empty Financials card with that reminder shown. When the contract comes in later, create it and link them.
"A project isn't going anywhere — put it on ice."
Change its Status to On Hold on the project detail page. Bring it back to Active later when things resume.
"The payment schedule got set up wrong."
If the milestone has no money received: hover, click the ✕, confirm. Gone. Add it again correctly.
If it already has money received: void the receipt first (coming in the Receivables module), then delete the milestone.
"We invoiced for something and got paid."
Temporary flow: Project → Payment Schedule tab → Record receipt → enter amount → applies top-down.
Future flow: When Receivables ships, this moves to a proper receipt entry module with receipt numbers, dates, instruments, and payer names.
"A new status is needed — e.g. 'Awaiting Client Signoff'."
Go to Masters → contract_status → add the label. It appears in every filter and dropdown immediately. No developer needed.
"Who changed this number?"
Project detail → Audit tab. Every change is there with who, when, and before/after.
"I can't see a module in my sidebar."
Your role doesn't have the {module}.read permission. Talk to your super admin, or check Admin → RBAC if you have the rights yourself.
20 — Quick answers
Frequently asked questions.
A contract is the legal document — what the client signed. A project is a unit of work — one service we deliver. A contract can fund multiple projects (e.g. one contract covers both Arch and Interior), and a project can be funded by multiple contracts (e.g. phased building). The two are linked but distinct.
Because "signed" doesn't describe where a contract is in its lifecycle — it describes an event. A contract can be Active and signed, or Active and not yet signed. The date field tells you whether it's signed; the status tells you where it's at in its life.
The Fee input goes red and the Save button disables. You literally can't save it. You'd need to either increase the contract value via a Value Revised amendment, or reduce the milestone fee.
No — you terminate it. Deleting would lose the audit trail, which is a non-starter. Terminate keeps everything visible with a status badge.
Both. The PM's number is their weekly judgment of the whole project. The deliverable count is the hard count of milestones marked done. They can diverge — that's a useful signal. If they diverge wildly, it's worth a conversation.
The platform rejects it with a clean error telling you by how much you exceeded. You'd need to add more payment schedule items first, or reduce the receipt amount. Receivables lets you park an unmapped receipt until the schedule catches up.
Click the notification in the bell dropdown. It takes you to the relevant page — Approvals for pending sign-offs, Receivables for new receipts, Meetings for new action items, and so on.
Yes. Both the Project ID and Project name are editable from the project detail page. Your edits are audited.
Payment Schedule = the planned milestones on a project with their fees (what we expect to invoice). Receivables = the actual money coming in from clients, with mappings back to schedule rows. Payout = per-project config for who on TPA gets a cut (with rate periods + exclusions). Commission Ledger = the append-only book of every commission rupee actually recognised, one row per (receipt × role-holder). Four different things, four different flows.
The Studio concept was removed on 22 April 2026 (migration 0053). The old "one row per studio with a partner and a P&L share" layer was never load-bearing — no compute read it. Profit categorization is now per-employee via a payout_category field on the Employee master. Cleaner data model, same business outcome. The old "Anahita/Mahyar/Carl studios" are gone from the platform; the partners themselves are still employees with payout_category = pnl.
It depends on the target type. Most amendments/remaps/voids use any semantics — one approver with the right permission is enough. Ledger corrections and period close use all semantics — every listed approver must independently sign off. In every case, the proposer cannot also be an approver.
Two steps. First, make sure the person is in the Employee master with an email on the row. Then go to their employee detail page → Platform Access → Enable. Pick a role, click Provision. The system creates a Supabase Auth user and hands you a temp password to send them. See Admin for the full flow.
First check the Audit tab on whatever you're looking at — the change might have a valid explanation. If it doesn't, ping Abhas directly. Never edit something you don't understand just to "fix" it.
21 — Reference
Glossary.
The words TPA Ops uses, in plain English.
- Contract
- The legal document signed with a client. Has an ID, a value, services, and dates.
- Project
- A single unit of work. One service = one project. Can be funded by one or more contracts.
- Service / Offering
- Architecture, Interior, or Municipal. A contract can bundle multiple; a project is always exactly one.
- N:M (many-to-many)
- The fact that one contract can fund many projects, and one project can be funded by many contracts. The platform was built for this from day one.
- Link / Allocation
- The connection between a contract and a project. Each link has an allocated ₹ value.
- Payment Schedule
- The ordered list of milestones we bill against, per project per contract.
- Milestone / Deliverable
- An entry in the payment schedule. Has a name, a fee, a % of the contract, a work status, and a received amount.
- Receipt
- Money coming in from a client.
- Waterfall
- The algorithm that fills a payment schedule from the top when a receipt is applied.
- Payout category
- One of
pnl,mgp,variable,normal, or blank. Set on the employee master, drives how Payout and Commission Ledger treat that person. - PNL (payout category)
- Partner profit share — flag-only, no per-receipt rate. Used for partners who take a cut of project profit at settlement time.
- MGP
- Minimum Guarantee Payout — an employee category where the per-project rate is compared against the employee's default commission %. Any deviation is recorded as an override with a required reason.
- Variable (payout category)
- Ad-hoc bonus employees. Per-project rate is typed manually, with no house-rate comparison.
- Rate period
- A date-effective rate inside a payout assignment. "15% from 1 Jan to 28 Feb; 17% from 1 Mar onwards." Historical rates are preserved so old receipts never silently re-rate.
- Receipt exclusion
- A specific receipt removed from a specific payout assignment, with a required reason. Shows up on the Ledger view with the reason visible.
- Commission role
- A named role on a contract — AP (Associate Principal), PA (Principal Associate), Finder, Converter — each with an optional rate override.
- Home offering
- An employee's primary service (ARCH / INT / MUNI). Constrains which contracts they can hold an AP/PA commission role on.
- Append-only
- A table where UPDATE and DELETE are blocked at the database level. Corrections are new offsetting rows, not edits.
audit_logandcommission_ledger_entriesare append-only. - Platform access
- The ability to sign in. Provisioned by super admin from the Employees module — creates a Supabase Auth user, links it to the employee row, assigns a role. Employees exist on the master independently of whether they can log in.
- RBAC
- Role-Based Access Control. 52 permissions across 13 modules. Each role has a set of permissions; each user has one or more roles.
super_adminbypasses all checks. - Terminate
- End something prematurely with a reason. Non-destructive — keeps the audit trail.
- Dropdown Master
- The central lists that drive every dropdown on the platform. Add once, use everywhere.
- Audit trail
- The complete log of who changed what, when, and why. Baked into every table on the platform.
- Approval
- A gated action that needs sign-off from one or more approvers.
- Hero case (Scenario C)
- Nickname for the "2 contracts, 1 project" pattern — the one that broke Excel forever.
- Staging
- A copy of the live site at
staging-ops.64-227-180-82.nip.iowhere new code gets tested before going to the real site. Separate database, separate everything — touching staging cannot touch production. - Heartbeat
- An automatic check that runs every 5 minutes from GitHub's servers (not from our server) to verify the live site is up. Emails the repo owner and opens a GitHub issue on failure.
- Parked decision
- A design question we've deliberately left open because answering it wrongly is more expensive than waiting. Documented so nobody is surprised and nobody "fixes" them silently.
22 — Behind the scenes
Life of a change.
How code and data moves from a laptop, through staging, and into the live site — with the checks and safety nets at each step.
This section is for anyone curious, and for whoever takes the platform over in future. The deployment machinery isn't something users touch directly — but understanding it makes the platform feel less like a black box, and makes incident response faster when something breaks.
scripts/pre-deploy-smoke.sh after the container restart, and have it exit 0, before the deploy is considered done. The script mints a real Supabase JWT and probes 32 endpoints + 4 per-tier UAT user dashboards. Any 5xx or unexpected status aborts the deploy and triggers an image rollback to the :rollback tag. The full mandate is in docs/pre-deploy-checklist.md; the public-facing why is the May 3 staging deploy that ate three silent bugs CI didn't catch (Supabase service-role-key missing from compose env, JWT lacking role claims, Pydantic schemas typing dates as strings). CI alone misses deployed-environment bugs — the smoke gate is the safety net.
The mental model: four stacked copies
Think of four stacked copies of the system. Each copy is identical in shape, different in purpose.
| # | Name | Where it runs | Who uses it | What's in the database |
|---|---|---|---|---|
| 1 | Local | Abhas's laptop | Abhas (and any future dev) while building | A small synthetic seed |
| 2 | CI | GitHub runners, ephemeral | Nobody — it exists only long enough to run tests | A throwaway seed |
| 3 | Staging | staging-ops.64-227-180-82.nip.io | Abhas + reviewers during UAT | An empty mirror — same shape as prod, no real money |
| 4 | Production | ops.64-227-180-82.nip.io → ops.tparch.net | The real TPA team | The real data |
A change starts in #1 and ends in #4. Each step has a purpose, a check, and a safety net.
The flow at a glance
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ 1 LOCAL │ ───▶ │ 2 CI │ ───▶ │ 3 STAGING │ ───▶ │ 4 PROD │
│ laptop │ │ GH Acts │ │ droplet │ │ droplet │
└─────────┘ └─────────┘ └─────────────┘ └─────────────┘
│ │ │ │
(write code) (run the tests) (click the feature) (real money lives here)
│ │ │ │
▼ ▼ ▼ ▼
one laptop 5 parallel jobs public URL on nip.io public URL (nip.io today,
Postgres in must ALL pass zero-downtime deploy tparch.net after DNS)
Docker before merge on every push to
`staging` branch deploys only from `main`
Stage-by-stage
1Local — write the code
One command boots the whole stack on the laptop: ./scripts/local-up.sh.
Under the hood: Docker Desktop boots Postgres (on port 5433) and Redis (on port 6379). Alembic runs all 53 migrations against the empty Postgres. A seed script inserts dummy employees, dummy contracts, a super admin account. The API starts on port 8002. The Next.js frontend starts on port 3000.
http://localhost:3000/login, type any email, click Continue. In as super admin = healthy.local-down.sh && local-up.sh rebuilds in 60 seconds.1bBranch, code, commit, push
New work always goes on a branch, never directly to main.
main (live, production) │ ├─ staging (mirror of what's queued for prod) │ └─ sprint/28-my-feature ← work happens here
ruff, mypy, pytest, pnpm build), pushes the branch. Nothing on the branch touches any live copy. It's private until a PR is opened.2CI — the robot reviewer
The moment a branch is pushed, GitHub Actions runs .github/workflows/ci.yml. This is a 5-job fan-out that takes about 6 minutes.
┌──────────────────────────┐
│ Push to GitHub triggers │
└────────────┬─────────────┘
│
┌───────────────┬───────┴────────┬──────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ ┌──────┐
│ api │ │ web-build │ │ playwright │ │ docker- │ │ done │
│ tests │ │ typecheck │ │ smoke E2E │ │ builds │ │ (ok) │
└─────────┘ └────────────┘ └────────────┘ └──────────┘ └──────┘
ruff+mypy+ TypeScript 5 browser docker build aggregator —
102 pytests compiles, test flows both api + passes only
w/ Postgres Next.js builds (login, web Docker if all 4 above
+ Redis without errors protected images passed
routes,
etc.)
main branch protection requires all 5 green before merge. Even a super admin can't push to main without CI passing.2bMerge to staging — deploy to the preview
When CI is green and the PR description checks out, the branch is first merged into staging. This triggers the staging deploy workflow.
staging branch push
│
▼
Build Docker images, tag with commit SHA
│
▼
Push images to GitHub Container Registry (ghcr.io)
│
▼
SSH into droplet (64.227.180.82)
│
▼
docker compose -f docker-compose.staging.yml pull
docker compose -f docker-compose.staging.yml up -d
│
▼
Run Alembic migrations against the staging Supabase project
│
▼
Health check: curl staging-api.64-227-180-82.nip.io/health
https://staging-ops.64-227-180-82.nip.io/login works. /health returns {"status":"ok","env":"staging"}.fvczgarmtzeorkhpeyss) and a separate empty mirror database. Nothing that happens on staging can touch production data.3Merge staging → main — the real deploy
When staging is proven good, staging is merged to main. This triggers the production deploy workflow.
main branch push
│
▼
Build Docker images, tag with commit SHA (e.g. sha-aaa2e8b)
│
▼
Push to ghcr.io
│
▼
┌─ APPROVAL GATE ─┐
│ GitHub Env: │ ← requires a human to click "Approve and deploy"
│ production │ in the GitHub UI before the rest of the job runs
└────────┬────────┘
▼
SSH into droplet
│
▼
docker compose -f docker-compose.production.yml pull
docker compose -f docker-compose.production.yml up -d
│
▼
Run Alembic migrations against the prod Supabase project
│
▼
Health check: curl api.ops.64-227-180-82.nip.io/health → expect env=prod
api.ops.64-227-180-82.nip.io/health returns {"status":"ok","env":"prod"}. The running container's SHA tag matches the commit on main.- The human approval gate stops a deploy that shouldn't go out.
- Docker images are SHA-tagged. Rolling back is
docker compose pull :sha-<previous> && up -d. The old image is still in the registry. - A nightly pg_dump runs on the droplet, so even a migration that eats data has a ~24h RPO of hand-rollable Postgres dumps.
4After deploy — heartbeat watches it
Every 5 minutes, a GitHub Actions workflow called heartbeat.yml runs from a GitHub runner (not from the droplet — a droplet-local check can't detect a droplet outage). It hits 4 URLs: prod web, prod API, staging web, staging API. For API endpoints, also asserts the body contains the correct env string.
If any check fails: GitHub emails the repo owner, and a second job opens (or comments on) a rolling heartbeat GitHub issue so dupes don't flood the inbox.
Where does each thing live?
| Thing | Local | CI | Staging | Production |
|---|---|---|---|---|
| Source code | Your checkout | Temp clone | Docker image | Docker image |
| Postgres | Docker :5433 | Service container | Supabase fvczgarmtzeorkhpeyss | Supabase widlzzmcwxilgzguqnmg |
| Redis | Docker :6379 | Service container | Droplet container | Droplet container |
| API | uvicorn :8002 | pytest subprocess | tpa-ops-staging-api-1 | tpa-ops-api-1 |
| Frontend | next dev :3000 | next build | tpa-ops-staging-web-1 | tpa-ops-web-1 |
| URL | localhost:3000 | n/a | staging-ops.64-227-180-82.nip.io | ops.64-227-180-82.nip.io |
| Logs | /tmp/tpa-ops-*.log | Actions UI | docker logs on droplet | docker logs + Sentry |
If something breaks
- Heartbeat opens a GitHub issue labelled
heartbeat. Go read the latest comment — it has the workflow run link. - Check
/healthmanually:curl https://api.ops.64-227-180-82.nip.io/health. Wrongenvor a fail = API or DB down. - Check Sentry (wired via Admin → Integrations) for any recent exceptions.
- Roll back: SSH to droplet →
docker compose pull tpa-ops-api:sha-<previous_good>→up -d api. Seedocs/rollback.md. - If the database is wrong: staging data is disposable; production has a nightly
pg_dumpon the droplet. Restore procedure indocs/runbook.md.
23 — Behind the scenes
Parked architectural decisions.
Some design questions have been deliberately left open. They're not bugs — they're known forks in the road we chose not to walk down yet, because walking down them wrongly is more expensive than waiting.
Writing them down explicitly so that (a) nobody is surprised, (b) nobody "fixes" them silently, (c) whoever picks them up later knows what the two sides of the question look like.
23.1 — Two parallel "who earns commission" worldviews
What's parked: We currently have two systems that answer the question "who gets paid commission on this receipt and how much".
| System | Where it lives | When it's used |
|---|---|---|
| Contract roles | contract_roles table with rate_pct per person per contract | Commission Ledger credits |
| Project payout assignments | With rate periods + exclusions | Per-project configuration and projection only (see §11 Payout) |
Today they run in parallel — the Payout tab doesn't feed Ledger credits. The Ledger uses the simpler contract-role rate.
23.2 — The pct convention asymmetry (percentage vs fraction)
What's parked: Two code paths store the same thing — a commission rate — using different number conventions.
contract_roles.rate_pctstores a percentage.15.0means 15%.employees.default_commission_pctstores a fraction.0.1500means 15%. The Ledger multiplies by 100 when it reads it.- The Payout module treats
default_commission_pctas a percentage scalar (9means 9%) for its equality comparison.
So the same column means 0.15 = 15% when read by the Ledger and 9 = 9% when read by the Payout module.
23.3 — Cross-employee cost allocation (the "Shefali case")
What's parked: At old TPA, some senior employees had part of their salary absorbed as a cost across multiple junior employees' commission calculations — so the junior's effective rate reflected the senior's pro-rata oversight cost. Bespoke to one or two situations.
What was done: When we killed the Studios model (Apr 22, migration 0053), we deliberately did NOT reimplement this. The "Shefali pro-rata", "Mahyar finder fee", "Mahyar salary bump" special cases all got dropped.
23.4 — The Receivables module temporary flow
What's parked: Today there's a Record Receipt button on the Payment Schedule tab that runs a top-down waterfall apply against milestones. The real Receivables module — proper receipt entity, bank reconciliation, manual splits, instrument tracking (cheque/UPI/RTGS) — is partly built (service + endpoint exist, /receivables page exists) but the UI polish and the auto-suggest mapping flow are not done.
24 — Behind the scenes
Uptime monitoring.
How we know the site is up — and how we find out the moment it isn't.
A GitHub Actions workflow called heartbeat runs every 5 minutes. It's deliberately not running on the droplet — if the droplet dies, a droplet-based checker would die with it and the outage would go silent. So it runs on GitHub's runners.
What it checks
The workflow pings 4 URLs in parallel:
| Target | URL | What it checks |
|---|---|---|
prod-web | ops.64-227-180-82.nip.io/login | Login page returns HTTP 200 |
prod-api | api.ops.64-227-180-82.nip.io/health | Response body contains "env":"prod" |
staging-web | staging-ops.64-227-180-82.nip.io/login | Login page returns HTTP 200 |
staging-api | staging-api.ops.64-227-180-82.nip.io/health | Response body contains "env":"staging" |
env assertion matters. It catches the scenario where Caddy reverse-proxies the wrong backend container and returns prod's health page for the staging URL (which actually happened once — see the deploy gotchas section of the internal notes). A plain HTTP 200 check would have missed it entirely.
When something fails
- GitHub emails the repo owner (Abhas) automatically — no extra wiring.
- A second job opens (or appends to) a rolling GitHub issue labelled
heartbeat. One issue per outage cluster, so the inbox doesn't flood with dupes.
On-demand trigger
To test the workflow without waiting 5 minutes:
gh workflow run heartbeat.yml
Or from the GitHub UI: Actions → Heartbeat → "Run workflow".
Known limit — schedule drift
GitHub's scheduled workflows are best-effort and can lag 5–15 minutes under load. That's fine for uptime awareness at TPA scale. If the platform ever needs sub-minute detection (it doesn't, today), layer UptimeRobot or BetterUptime on top — the heartbeat workflow doesn't need to be removed.
False alarms
If heartbeat opens an issue but the site is actually fine, it's probably a GitHub Actions scheduling hiccup or a 15-second transient. Close the issue. If it recurs within 30 minutes, treat as real and start the incident steps in docs/runbook.md.