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.

19 sections 45 min read end-to-end Last updated 11 May 2026

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.
Who this guide is for: everyone. If you're a super admin, read cover to cover. If you're a PM, skip to Status Updates. If you're in Finance, Contracts and Payment Schedule are your main pages. If you're lost, the FAQ and Glossary at the bottom will usually help.
What changed in May 2026 (v0.5 — shipped 10–11 May):
  • 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.
What changed in May 2026 (v0.4):
  • 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); currency and scope_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.

Client
Contract
Project(s)
Payment
Schedule
Receipts

Reading left to right:

  1. A client wants work done. They sign with TPA.
  2. A contract captures the deal — what services, how much money, when signed, for how long.
  3. Each service becomes a project. If a contract covers Architecture and Interior, that's two projects.
  4. Every project builds a payment schedule — the list of milestones we bill against, each with a fee.
  5. 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.

Don't see a module? Your role probably doesn't have permission. Ask your super admin.

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

Why this matters: You don't need a developer to add a new status, city, department, or designation. Just add it here and it shows up everywhere — filter dropdowns, employee/project/contract forms, the Excel import wizard's column matchers, reports, exports. Zero code changes.
Hard validation on Department, Designation, Project Status, Contract Status (May 2026): When you bulk-import employees / projects / contracts, the validate step rejects rows whose values aren't in the master with a clear hint pointing here. Seed the master before the import — or expect a row of red errors at validate time. The "soft" world where any string was accepted is gone for these four categories.

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.

  1. A client signs a contract. Someone at TPA creates the contract in the platform with the terms, value, and services.
  2. The contract covers one or more services. Each service needs a project. One contract = one or more projects.
  3. Projects and contracts get linked. The platform tracks which contract funds which project and how much money is allocated.
  4. Each project builds a payment schedule. Milestones like Concept Design, Schematic, DDs, CDs — each with a fee and a due date.
  5. Work happens. The PM updates progress weekly and marks milestones as in_progress or completed.
  6. Money comes in. Recorded as receipts, which fill the payment schedule from the top down.
  7. 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 IDA manual free-text ID. TPA's internal code like 2425/254/RJV/YJN or anything else you want.
ClientThe company name.
LocationSite location.
StatusDraft, Active, On Hold, Closed, or Terminated. Colour-coded via the Dropdown Master.
OfferingArchitecture, Interior, Municipal — one or more.
Project TypeResidential, Commercial, Hospitality, etc.
Contract ValueTotal in rupees.
SignedEither a date badge (if signed) or "Not signed".
Area / RateSquare feet and ₹/sq ft.
Start / EndThe 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 revisedChange the number with a reason. No scope change.
Scope addedBump the value because the client asked for more work in an existing service.
Service addedStarted Arch-only, now adding Interior. Usually bumps the value too.
Service removedClient 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:

Summary cards
ServiceThe service badge (ARCH / INT / MUNI).
StatusActive / On Hold / Completed / etc.
Project ValueTotal money from all active linked contracts.
Work ProgressThe 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.

Why this matters. Earlier the platform had value in three places — the contract, the allocation on the contract-project link, and a derived project total. They drifted. Migration 0054 (April 2026) locked Project Value as the authoritative number and turned the other two into alignment signals.

Where the value comes from

Entered directlyYou 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 contractWhen 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:

● AlignedProject Value equals the sum of allocations. The happy path — nothing to do.
▲ Over-contractedSum 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-contractedSum of allocations < Project Value. The contract(s) aren't yet covering the full project budget. Expected for projects mid-negotiation.
○ No value setProject Value is blank but contracts are linked. A Seed from allocation button appears — click it to pull the allocated total in.
Freeze-on-received. Once a milestone has received money, its fee is frozen. Editing Project Value will not recompute fees that already have 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.

Important: Each payment schedule item belongs to one specific contract. If a project is funded by two contracts (phased building), you build the schedule separately for each contract.

The columns

#Sequence — the order milestones run in.
MilestoneThe name (e.g. "Concept Design").
ContractWhich 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

  1. Click "Add item" in the top right.
  2. Pick a contract from the dropdown. A helper banner appears: Contract X · Value ₹50L · Allocated ₹15L · Remaining ₹35L.
  3. Type the milestone name like "Concept Design".
  4. Type either % or Fee — the other fills in automatically based on the contract value.
  5. 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.

AmountHow much money came in.
ContractOnly shown if the project has more than one linked contract. Otherwise auto-picked.

Click Apply. The amount waterfalls from the top:

The waterfall in action

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 IDThe code.
Project nameThe friendly name.
Client / LocationWho it's for and where.
ServiceARCH / INT / MUNI badge.
ProgressA mini progress bar with the current %.
Last updatedWhen the PM last moved the needle.
UpdateA 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.

What's not here: Contract values, allocated amounts, received money, payout config, commission splits, fee rates. Nothing financial. A PM who shouldn't see money-stuff can do their entire job from this one page.

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

Sales-leads pipeline is NOT here (it was, until 9 May). Sales analytics live on /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 OwnerScope to one owner's raises (employees who own at least one project that has raise data).
Time horizonNext 3 / 6 / 12 months or All months.
Show dismissedOff 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.

ProjectName + 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.
% CompleteProject-level overall progress (from the Tracker). Click-through to the Tracker.
BilledWhat percentage of project value has been received. Tooltip: ₹X of ₹Y.
PendingProject value − received, in gold.
Raise (target)The raise amount + target month.
ConfidenceEditable inline. Drop a Confirmed → Uncertain and the KPI cards above recompute immediately. Every flip is audited.
WeightedRaise × confidence weight.
Dismiss / RestoreMark 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

Confirmed100% — invoice raise is committed (signed-off scope, partner agreement).
Likely70% — high likelihood but not yet committed.
Uncertain30% — 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.
Two parallel sources of commission truth. Right now the Payout module is config-and-projection only — it computes what each person would earn per receipt, but it does not write to the commission ledger. The ledger (commission_ledger_entries) still reads rate from contract_roles.rate_pctemployees.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.

Appraisal demo on NM-C-TOWER: Shefali starts at 15% from 1 Jan through 28 Feb (closed period). On 1 March she's bumped to 17%. Add a new period with 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 totalsreceipt_total is per-(person, project) — sum of NOT-excluded receipts for that specific assignment. It is not project-wide.
Strict backward period matchIf 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 receiptsExcluded rows appear with the reason, don't contribute to receipt_total, and have due = 0.
Uncovered receiptsStill 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 levelThe 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.

Where to poke: The richest demo surface is /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.

Append-only, enforced in Postgres. The 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 from contract_roles.rate_pct, or employee_default = fell back to employees.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:

  1. A receipt comes in, gets mapped to a project deliverable via Receivables.
  2. For each commission role on the underlying contract (AP, PA, Finder, Converter), the ledger service resolves a rate using the precedence rule below.
  3. 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

  1. contract_roles.rate_pct — per-contract override on a role. Percentage scalar (15.0 = 15%). Source label: manual_role_rate.
  2. 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 IDAuto-generated. Sequential.
DateWhen the money landed.
Amount (₹)What came in.
PayerThe client entity that paid. Can be a company name, a proprietor, anyone.
InstrumentNEFT / RTGS / UPI / Cheque / Cash. Used for reconciliation with bank statements.
Mapped toWhich project deliverable(s) this receipt's money hit. Can be one or many if the waterfall split a single receipt.
StatusMapped / 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 itselfNew 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.

Remember the waterfall. Amount applies top-down to the project's deliverables. If the first deliverable has ₹2 L capacity left and the receipt is ₹5 L, ₹2 L goes to milestone 1, the next ₹3 L flows to milestone 2, and so on. Full waterfall example in Payment Schedule.

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

noopA test target used for wiring and drills. Approving does nothing.
contractCreating a contract that needs sign-off (rare — most creates don't gate).
contract_amendmentEvery amendment (Value Revised / Scope Added / Service Added / Service Removed) goes through here.
receipt_remapMoving a receipt from one project to another.
receipt_voidVoiding a receipt.
ledger_correctionPosting a correcting pair in the commission ledger. "All" semantics — every listed approver must sign off.
period_closeClosing 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.
Self-approval is blocked. If you proposed the request, the Approve button is hidden. This is enforced at the service layer, not just the UI — trying to bypass via the API returns a 403.

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:

DescriptionWhat needs to happen.
OwnerWho's responsible. Picked from the employee master.
Due dateBy when.
StatusOpen / In Progress / Done / Cancelled.
Linked entityOptional — 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.

Why this matters: Before the platform, action items died in WhatsApp threads and nobody knew what was open. Now every action item has an owner, a date, a status, and a permanent audit trail. If something slips, the chain is visible.

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.

Terminating an employee is non-destructive. Set their termination date; their row stays in the master forever. Historical contracts, ledger entries, and audit rows still reference them. The list filters them out of Active by default. Don't try to delete — you'll lose the audit trail.

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.

super_admin bypasses all permission checks. Only grant this to 1–2 people, full stop. Every other role should be explicit about what it can and can't do.

Platform access provisioning

User accounts are admin-provisioned, not self-registered. The flow:

  1. Make sure the employee is in the Employee master with an email on the row.
  2. Go to Employees → open the person → Platform Access card → Enable access.
  3. Pick their role (usually from the role list you've built on the RBAC tab).
  4. Click Provision. The platform calls Supabase's Admin API, creates an auth user with a temp password, creates the local users row, links it to the employee via employee.user_id, and assigns the role.
  5. 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.

Why this is decoupled from RBAC role. A "Finance Manager" RBAC role doesn't necessarily imply the Finance dashboard view — sometimes they want the Partner view of cash flow. Keeping tier as a separate field gives admin full discretion without role-juggling.

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.

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.

Shape A Simple — 1 contract, 1 project

The most common case. Single service, single project, one-for-one relationship.

NM-A-001 Contract · ₹20L
NM-A-PROJ Project · ARCH

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.

Shape B Multi-service — 1 contract, N projects

One legal document covers multiple services, each of which becomes its own project.

NM-B-001 Contract · ₹50L
ARCH + INT
NM-B-ARCH Project · ₹30L
NM-B-INT Project · ₹20L

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.

Shape C Phased funding — N contracts, 1 project

The interesting one. Multiple contracts fund the same physical project — usually in phases over time.

NM-C-PHASE1 Contract · ₹15L
Closed
NM-C-PHASE2 Contract · ₹25L
Active
NM-C-TOWER Project · ARCH
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.

Companion doc: A standalone PDF with one expanded block per scenario lives at 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
How to use this matrix. Find the row that matches the project you're looking at. Compare columns 5–6 (Chip + On screen) to reality. If they don't match, take a screenshot and flag it — that's a bug, not a workflow question. Column 7 (Do this) is your next action; never "just edit the value" without reading that column.

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.

1 Brand new project — nothing signed yet

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.

2 Estimated budget — pre-signing

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.

3 Advance arrived before the contract — retainer/goodwill money

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.

4 Contract signed, matches estimate

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.

5 Contract came in higher than your estimate

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.

6 Contract came in lower than your estimate

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.

7 Contract linked, Project Value never typed (the Godawari case)

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.

8 Phase 1 + Phase 2 — two contracts on one project

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.

9 Over-committed multi-contract

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.

10 Bundled contract — one contract covers multiple projects

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.

11 First invoice raised and received

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.

12 Scope change — editing Project Value mid-project

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.

13 Fully received — project paid in full

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.

14 Contract terminated mid-project

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.

15 Contract amended — value revised upward

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.

16 Seed clicked — row 7 resolved

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.

17 Project Value cleared back to empty

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.

18 Zero-value project (explicit ₹0)

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.

19 All contracts terminated

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.

20 Orphan contract — signed but not linked to any project

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.

One rule that ties everything together. 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 Masterscontract_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_log and commission_ledger_entries are 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_admin bypasses 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.io where 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.

Mandatory pre-deploy smoke gate (locked 2026-05-03). Every staging and prod deploy must run 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.

#NameWhere it runsWho uses itWhat's in the database
1LocalAbhas's laptopAbhas (and any future dev) while buildingA small synthetic seed
2CIGitHub runners, ephemeralNobody — it exists only long enough to run testsA throwaway seed
3Stagingstaging-ops.64-227-180-82.nip.ioAbhas + reviewers during UATAn empty mirror — same shape as prod, no real money
4Productionops.64-227-180-82.nip.ioops.tparch.netThe real TPA teamThe 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.

Achieves A private, disposable copy of the platform Abhas can break without consequence.
Check Open http://localhost:3000/login, type any email, click Continue. In as super admin = healthy.
Safety net Postgres data lives in a Docker volume. If anything goes sideways, 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
Achieves Abhas writes code, runs the local gates (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.)
Achieves Catches 95% of bugs before a human ever looks at the PR.
Check The GitHub PR page shows a green ✓ next to each of the 5 jobs. Any red — click in to see which test or lint failed.
Safety net 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
Achieves The new code is running on a public URL, against a staging database, where Abhas or a reviewer can click through the feature as a real user would.
Check https://staging-ops.64-227-180-82.nip.io/login works. /health returns {"status":"ok","env":"staging"}.
Safety net Staging has a separate Supabase project (fvczgarmtzeorkhpeyss) and a separate empty mirror database. Nothing that happens on staging can touch production data.

3Merge stagingmain — 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
Achieves The new code is live on the real site for the real TPA team.
Check 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.
Safety nets (three layers)
  1. The human approval gate stops a deploy that shouldn't go out.
  2. Docker images are SHA-tagged. Rolling back is docker compose pull :sha-<previous> && up -d. The old image is still in the registry.
  3. 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.

See also §24 Uptime monitoring for the full story.

Where does each thing live?

ThingLocalCIStagingProduction
Source codeYour checkoutTemp cloneDocker imageDocker image
PostgresDocker :5433Service containerSupabase fvczgarmtzeorkhpeyssSupabase widlzzmcwxilgzguqnmg
RedisDocker :6379Service containerDroplet containerDroplet container
APIuvicorn :8002pytest subprocesstpa-ops-staging-api-1tpa-ops-api-1
Frontendnext dev :3000next buildtpa-ops-staging-web-1tpa-ops-web-1
URLlocalhost:3000n/astaging-ops.64-227-180-82.nip.ioops.64-227-180-82.nip.io
Logs/tmp/tpa-ops-*.logActions UIdocker logs on dropletdocker logs + Sentry

If something breaks

  1. Heartbeat opens a GitHub issue labelled heartbeat. Go read the latest comment — it has the workflow run link.
  2. Check /health manually: curl https://api.ops.64-227-180-82.nip.io/health. Wrong env or a fail = API or DB down.
  3. Check Sentry (wired via Admin → Integrations) for any recent exceptions.
  4. Roll back: SSH to droplet → docker compose pull tpa-ops-api:sha-<previous_good>up -d api. See docs/rollback.md.
  5. If the database is wrong: staging data is disposable; production has a nightly pg_dump on the droplet. Restore procedure in docs/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".

SystemWhere it livesWhen it's used
Contract rolescontract_roles table with rate_pct per person per contractCommission Ledger credits
Project payout assignmentsWith rate periods + exclusionsPer-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.

Why parked Both shapes are correct for some truth. Merging naively would overwrite one perspective with the other. Reconciling properly is a full-session architectural decision. Cost today Users might set up a rate on the Payout tab and expect it to change what the Ledger computes. It doesn't (yet). Training and docs mitigate. Trigger to unpark Someone gets paid wrongly by the Ledger because the Payout tab said different. So far, not yet.

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_pct stores a percentage. 15.0 means 15%.
  • employees.default_commission_pct stores a fraction. 0.1500 means 15%. The Ledger multiplies by 100 when it reads it.
  • The Payout module treats default_commission_pct as a percentage scalar (9 means 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.

Why parked Fixing means a data migration plus two code path changes plus every test re-run. The mismatch is localized, documented in code comments, and hasn't caused a production bug. Cost today One extra footgun for anyone joining the codebase. Documented in CLAUDE.md rule 12. Trigger to unpark Merged into the 23.1 reconciliation — once we pick a canonical source, the unit convention comes with it.

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.

Why parked The special rules were tightly coupled to the Studios / CommissionPolicy architecture we removed. To bring them back, we'd need a new primitive — "salary cross-allocation across a set of employees" — which nobody has asked for since the kill. If TPA's comp philosophy requires it again, we'd design it deliberately instead of porting the old shape. Cost today P&L reports for those specific people look different than they did in the old Studios model. Known change, communicated during the Apr 22 rework. Trigger to unpark A partner asks "why does Shefali's payout look wrong" and the right answer is "because we didn't port that rule, here's what we'd need to build."

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.

Why parked The temporary flow is functionally correct for single-receipt, single-project cases — most of TPA's actual receipts. The full module needs UX decisions (manual splitting? dispute resolution?) that are easier to make after TPA is using the temporary flow for a while. Cost today Finance can't yet record a receipt that splits across milestones out of waterfall order. Workaround: temporarily mark milestones manually. Trigger to unpark First receipt that genuinely needs a non-waterfall split.

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:

TargetURLWhat it checks
prod-webops.64-227-180-82.nip.io/loginLogin page returns HTTP 200
prod-apiapi.ops.64-227-180-82.nip.io/healthResponse body contains "env":"prod"
staging-webstaging-ops.64-227-180-82.nip.io/loginLogin page returns HTTP 200
staging-apistaging-api.ops.64-227-180-82.nip.io/healthResponse body contains "env":"staging"
Why the 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.