13 September 2026 · 10 min read
From a Screenshot of a Table to a Live Ops Portal: Building Quero's Team Operating System


Most engineering write-ups start from a spec. This one starts from a photo of a table, taken on someone's phone, of who owes how many Instagram followers this week. That photo, and about a dozen more like it, is the actual starting material behind Quero's internal team portal, the tool that now runs daily accountability for a seven-person team: questions authored, leads generated, followers brought in, tech issues assigned, and a content calendar nobody has to ask about in a group chat anymore.
The brief showed up as a photo of a table, not a spec
There was no product requirements doc. There was a screenshot of a responsibilities table from a meeting, a PDF of minutes from a different meeting three weeks later with different numbers in it, and a running stream of voice-dictated corrections mid-build: “Yash Kanade's role changed, make it proper everywhere,” “followers means people you actually got to follow the account, not just a number,” “add Shubham to lead outreach too.” Building this meant treating every one of those as a real requirement, not noise to clean up before the real work started. That is what building software for your own team, while running that team, actually looks like day to day.
A login flow with no email field, on purpose
The portal's landing page is a grid of the team's actual photos, everyone knows what everyone looks like, so it doubles as the login screen. Click your own photo and it routes to `/login?id=EMP_04`, or whichever employee ID is yours. There's no username field to type into on that screen at all. The employee ID shows up locked, in a plain, non-editable badge, and the only thing left to fill in is a password, auto-focused so the cursor is already sitting there.

Under the hood, an employee ID is just the local part of an internal email, `emp_04@quero.internal`, that Supabase Auth treats like any other account. Locking that field isn't a security control by itself, someone could still edit the URL's `id` query param, it's a UX decision: nobody types their own ID wrong, nobody accidentally lands in someone else's login attempt, and there's exactly one thing left to get wrong, the password. The one piece of this that is a real security boundary lives elsewhere: only the super admin account can change anyone's employee ID or password at all, through a server action that uses a service-role Supabase client the browser never sees, gated by checking the calling user's own employee ID before it touches anyone else's.
Two Supabase projects, one dashboard
The portal needed a directory of every question the content team had ever written, broken down by exam and subject. Those questions already live in Quero's actual exam platform, a completely separate Next.js app with its own Supabase project. Duplicating that data into the ops portal's own database would have meant two sources of truth drifting apart the first time anyone edited a question in one place and not the other.
Instead, the portal holds a second, read-only, server-only Supabase client pointed at the exam platform's project, using a service-role key that never reaches the browser. Every server component that needs a live count queries the exam platform's tables directly, joins subjects, chapters, and topics in memory, and renders a directory that's never more than a page-load stale.
import "server-only";
import { createClient as createSupabaseClient } from "@supabase/supabase-js";
// Read-only, server-only client for the exam platform's Supabase project.
// A separate project from the portal's own database, used only to pull
// real question bank counts. Never imported from a client component.
export function createQueroAppClient() {
const url = process.env.QUERO_APP_SUPABASE_URL;
const key = process.env.QUERO_APP_SUPABASE_SERVICE_ROLE_KEY;
if (!url || !key) return null;
return createSupabaseClient(url, key, {
auth: { autoRefreshToken: false, persistSession: false },
});
}That "never imported from a client component" comment isn't decoration, it's the actual security boundary. The `server-only` import at the top makes it a build error, not a code-review nitpick, if anyone ever tries to pull that module into browser-bundled code.
Permissions that live in the database, not a hidden button
One requirement stayed exactly the same through every other change: only the co-founder should be able to reassign a tech issue or a sales lead to someone else. The obvious, wrong way to build that is to hide the "reassign" dropdown in the UI unless the logged-in user matches one employee ID. That stops a casual click. It stops nothing if someone opens the browser console and calls the Supabase client directly with the same credentials the page already has.
create or replace function public.only_smit_can_reassign()
returns trigger as $$
declare
smit_id uuid;
begin
select id into smit_id from public.profiles where employee_id = 'EMP_10';
if (new.assigned_to is distinct from old.assigned_to) and auth.uid() is distinct from smit_id then
raise exception 'Only the super admin can reassign this item.';
end if;
return new;
end;
$$ language plpgsql security definer;Hiding a button is a UX decision. A trigger that raises an exception on the one column that matters is a permission system. The two look similar in a demo and are nothing alike the moment someone tries to skip the UI entirely.
The same shape shows up for the content calendar: row-level security policies that check `auth.uid()` against exactly three employee IDs before allowing an insert, update, or delete, with a plain read policy open to everyone else. Whoever is signed in, whatever request they send, the database itself is the last word on who can change what.
Quotas that pile up instead of resetting
The daily question quota could have reset to zero every midnight, which is what most task trackers do by default. That was rejected on purpose. If someone logs three questions against a target of five, the missing two don't disappear, they get added to tomorrow's target. Miss four days in a row and the fifth day's dashboard shows exactly how far behind the pile actually is, not a fresh, forgiving zero that quietly erases the debt.
Try it below: step through a week where the target was hit some days and missed on others, and watch what "required to date" actually does.
Missed targets carry forward, they don't reset
5
Required to date
5
Actually logged
0
Piled up
Day 1: 5 logged against a target of 5, target met. That shortfall doesn't vanish at midnight, it gets added to tomorrow's target. Miss enough days in a row and the number in that last box is the honest, un-hideable answer to “are we actually on track.”
The numbers changed twice, on purpose
The first version of the daily quota came straight from a meeting-minutes PDF: forty questions a day total, split twenty NEET-UG and twenty JEE-Main, three named people contributing ten each. That shipped, got tested against the live database, and then got corrected days later, over a voice message, to a completely different shape: five people, five questions a day each, with a different set of names than the PDF had. Neither version was a mistake. The second one was the org's actual, current decision, and the first one was a real decision that got superseded, the same way a company's real headcount plan changes between one all-hands and the next.
| First version (from the MoM PDF) | Corrected version (shipped) | |
|---|---|---|
| Daily target | 40 total, split by exam | 5 per person, 5 people |
| Contributors | 3 named people, 10 each | 5 named people, 5 each |
| Follower tracking | A single self-reported number | The actual Instagram ID of every person referred |
| Source of truth | A PDF from one meeting | Corrected live, mid-build, by voice message |
The follower row is the more interesting correction. "Followers brought in" started as a plain integer someone typed in. It ended as a table of individual rows, one Instagram handle per referral, because a number can be inflated by typing a bigger number, and a list of specific accounts someone claims to have referred cannot be faked quite as casually. Small schema change, real behavioral difference.
A leaderboard built from everything, not one number
An "employee of the month" leaderboard is an easy feature to get wrong by picking one metric and ranking on it, which just teaches people to optimize that one number. This one is a composite: questions logged, followers referred, leads generated and outreached, tech issues resolved, and tasks finished before their deadline, each weighted differently, a converted lead worth more than a contacted one, a resolved issue worth more than an opened one, recomputed live on every dashboard load rather than batched overnight. Nobody can game a single column, because the score was never one column.
What actually broke
None of this shipped clean on the first attempt. The honest list of what went wrong, because it generalizes better than the parts that went right:
- A recurring content-calendar seeding script used `date.toISOString().slice(0, 10)` to compute the date string. `toISOString()` converts to UTC first, which shifts a local IST midnight back a day, so every Saturday post landed in the database tagged as Friday. Caught it by checking the actual weekday against what the script intended, not by trusting that a date object plus `.toISOString()` means what it looks like it means.
- Testing the LinkedIn-status restriction by signing in as an unauthorized employee and calling `.update()` directly returned `{ error: null, data: [] }`, no error at all. Postgres row-level security filters rows out of the `WHERE` clause silently; an update that matches zero rows because of RLS looks identical to a successful no-op update. The only real test is checking whether the row's value actually changed, not whether the client call threw.
- Every data table on the dashboard used `overflow-hidden` on its wrapper, which clips content instead of scrolling it. Fine on desktop where nothing was ever wide enough to clip. On a phone, a six-column table has nowhere to go, so it just cuts off mid-cell. The fix was a global search for that one wrapper pattern and a swap to `overflow-x-auto`, which was a five-minute change once someone actually opened it on a phone instead of assuming a responsive grid meant responsive tables too.
- The sidebar was a fixed 256px column with no mobile behavior at all, which on a ~400px-wide phone screen left less width for the entire dashboard than the nav bar itself was taking up. It needed to become an actual slide-in drawer, closing automatically on navigation, not just a narrower version of the same fixed column.
Frequently Asked Questions
Why not just use Notion or a spreadsheet for this?
For the read-only reporting parts, a spreadsheet would have been fine, and a lot of internal tools should stay spreadsheets for exactly that reason. What pushed this into a real app was the permission boundary: "only the co-founder can reassign work" and "only three specific people can edit the calendar" are rules a spreadsheet's sharing settings can approximate but not actually enforce against someone who has edit access to the sheet at all. Once a rule needs to survive contact with a user who technically has access but shouldn't use it a certain way, that's the signal to move it into a real database with real row-level security, not before.
Why restrict reassignment at the database level instead of just hiding the button?
Because the button isn't the only way to reach the database. Any authenticated client with the anon key, which is public by design in a Supabase app, can call `.update()` directly from a browser console. A UI-only restriction stops someone who doesn't know that. A trigger that raises an exception on the exact column stops everyone, including someone who does.
Did the team push back on being tracked this closely?
The framing that mattered was making the targets visible and specific rather than vague and constant. "5 questions a day, this exact person, this exact quota, visible on your own dashboard when you log in" reads as clearer, not stricter, than an open-ended "try to contribute regularly" that nobody can actually measure themselves against. The pushback that did happen was about which five people and which five questions, the corrections covered earlier, not about the existence of the tracking itself.
The actual takeaway
None of the individual pieces here are novel: a read-only cross-project client, a database trigger, a quota that carries forward, a composite leaderboard. What's worth taking away is that the requirements never arrived as requirements. They arrived as a photo, a PDF, and a stream of corrections shouted across a build session, and the actual engineering work was building something flexible enough that a role rename, a quota model that got scrapped and replaced, and a definition of "follower" that changed shape entirely could all land as small, contained changes instead of rewrites. That's the real skill in building tools for a team you're also running: not getting the first version right, but making the second and third versions cheap.