Case study · Agency operating system

Aslam HQ: the system that runs my own agency

Aslam HQ is the product I built for myself and use in my own agency. It tracks LinkedIn outreach from the first connection request to a won project, sends email from our own domain, writes and schedules social posts, and keeps clients and staff in one place. Everyone signs in on the same page and sees only the part that is theirs.

The Aslam HQ dashboard with sample data: client, employee, LinkedIn profile and mailbox counts above a table of outreach results for each LinkedIn profile

Aslam HQ is private and used inside my agency. Every screen on this page shows invented sample data, not real clients or real LinkedIn numbers.

Product
Aslam HQ, an operating system for a small agency
Built by
Aslam Sarfraz, for his own agency
Who it is for
Small agencies, their clients and their staff
Status
In use in my own agency
First commit
27 June 2026
Core stack
Next.js, React, Supabase, Claude, Resend

For everyone

The short version

A small agency runs on a lot of loose ends. Leads sit in one tool, LinkedIn replies in another, email in a third, and clients and staff ask for updates in chat. I built Aslam HQ so all of it lives in one place.

For LinkedIn outreach, HQ records every person I contacted, who engaged, who replied and who became a client. It reads my LinkedIn inbox through a Chrome extension, matches conversations back to leads, and shows reply rates for each message template. Follow-ups come up on the day they are due.

Mailboxes send email from our own domain, with a record of every message and its delivery status. Social Engagement writes LinkedIn and X posts with Claude, lets me edit them, and posts or schedules them.

HQ is multi-user. I give clients and employees a login, and their portal shows a client their own project and payments, or an employee their own tasks. Nothing else is visible to them.

  1. Reach out and keep the record

    Every contact, reply and follow-up in one pipeline, from the first request to a won project.

  2. Send and post from one place

    Email from our own domain, and LinkedIn and X posts that are written, checked and scheduled.

  3. Share only the right part

    Clients and employees sign in to a portal that shows just their own work.

How a contact moves through the pipeline

Every lead sits at one of nine stages. Some moves happen on their own, such as an accepted connection becoming a lead. Replies, interest and wins stay my own call, so the numbers never claim more than I know.

  1. NewSaved, not yet contacted
  2. Social engagedLiked or commented first
  3. ContactedA real message went out
  4. Follow-upNot now, back on a set date
  5. RepliedThey wrote back
  6. InterestedThey want to talk
  7. MeetingA call is booked or held
  8. WonBecame a client
  9. LostClosed without a project

For founders and CEOs

The business case

What the agency needed, why HQ looks the way it does, and what has come out of it so far.

The problem

Tools that never talk to each other

Leads in a sheet, replies in LinkedIn, mail in another app, tasks in a chat. Nobody can say where a deal stands without opening four tabs.

Outreach with no memory

Without a record of who was contacted, who engaged and who replied, each campaign starts from zero and nobody learns which message works.

Clients and staff need a window, not the keys

A client wants to see progress and what is owed. An employee wants their tasks. Neither should see the agency's pipeline, or each other's work.

Why I built it this way

I built Aslam HQ for my own agency first, with small agencies in mind. Off-the-shelf tools each solve one slice, so a small team ends up with five products that do not share data.

So HQ starts from the work, not from a tool. A lead becomes a client, a client gets a work log and payments, a task goes to an employee, and the same login page serves everyone.

Because I use it myself, I see straight away what is slow or confusing and fix it. That is how it grew from a LinkedIn lead log into the system described on this page.

Aslam HQ analytics with sample data: total leads, messaged, got a reply, interested, meetings booked, projects won and follow-ups due, each with its percentage of the step before

One login for everyone, a different view for each

The owner decides, person by person, which LinkedIn profiles, clients, employees and mailboxes a login can reach. A teammate working on outreach never sees client payments. A client sees their own project and nothing else.

That is also what makes HQ usable by other small agencies: the same system serves the owner, the team, the clients and the staff, each with the right window.

What the owner can grant, per person
  • Specific LinkedIn profiles
  • Specific clients
  • Specific employees
  • Specific mailboxes, one by one

What each person sees

Pick a role to see the real screen that person gets. Each tab is the same app, signed in as a different kind of login.

Everything, across every workspace. The owner is also the only one who can create profiles, add logins and open the Admin Panel.

  • Clients and EmployeesCreate, edit and delete records
  • Lead AcquisitionEvery LinkedIn profile
  • MailboxesEvery mailbox, and who may use it
  • Social EngagementOwner only, it posts as the owner
  • Admin PanelLogins, grants and AI settings
The owner's Aslam HQ dashboard with every section in the sidebar, on sample data
The owner's dashboard: clients, staff, LinkedIn profiles and mailboxes, with outreach results for every profile side by side.

What it has returned

Aslam HQ runs inside my agency today. Outreach, email, posts, client records and employee tasks all live in it, and clients and staff use their own portals.

It has also changed how I work: I can see which message got replies and which follow-ups are due, and clients can check progress and payments themselves.

What it does today
  • Tracks LinkedIn outreach from request to won project
  • Reads the LinkedIn inbox and matches replies to leads
  • Sends email from our own domain, with a delivery record
  • Writes, schedules and posts to LinkedIn and X
  • Gives clients and employees their own portal

My role

Aslam HQ is my own project. I designed it around how my agency works, built it, and use it with my team and my clients.

I decide what gets built next, which is why it keeps growing.

What I own
  • The product design and what goes in next
  • The app, the database and the access rules
  • The two Chrome extensions
  • The AI features and what they cost
  • Running it with my own team and clients

How it grew

From a single-purpose LinkedIn lead log to an operating system for an agency, in about fourteen weeks. The dates come from the project's own history.

  1. A lead log and a Chrome extension

    The first version: save LinkedIn leads with an extension, catch duplicates, and count daily, weekly and monthly connection requests.

  2. The pipeline gets sharper

    Social engaged and Interested stages, inline status editing and bulk actions on many leads at once.

  3. Profiles, teammates and the Admin Panel

    A separate workspace for each LinkedIn profile, team logins, and one place where the owner manages who can reach what.

  4. A workspace for the whole agency

    Clients and employees with their own portals, prospect lists, and the LinkedIn inbox read into HQ and matched back onto leads.

  5. Each login gets only what it was granted

    Access moved from one big switch to a grant for each section, so a client sees their project and an employee sees their tasks.

  6. Mailboxes

    Email from our own domain with a daily sending limit, and a gap closed that would have let someone make themselves an owner.

  7. AI, with a price tag

    Choose the Claude model for each profile, summarise a contact's company, and see what every call cost.

  8. Social Engagement

    Write, draw and schedule LinkedIn and X posts, for the owner only.

For engineers and technical leads

How it's built

A Next.js app that talks to the database as the signed-in person, a thin server for the jobs that need a secret key, and rules inside the database that decide who sees which row.

How Aslam HQ is put togetherOwnereverythingTeammategranted partsClientown projectEmployeeown tasksThe web appNext.js · Reactone login page for allreads and writes its own data directly, as the signed-in personNext.js serverMiddlewaresession fresh? sectiongranted? then through.API routes: admin · mail · social · AIevery requestSupabaseAuthsessions and loginsPostgres33 tables, ruleson every onefilesStoragepictures, notesservice key,server onlyLinkedInyour own open tabreads your tab2 Chrome extensionsLead AcquisitionMessage Trackersigns in as you, writes through the same rulesResendyour own domainClaude APIwrites and summarisesLinkedIn and Xofficial APIs, OAuthSchedulercalls /api/social/cronThe browser talks tothe database as you.Only jobs that need a secretkey go through the server.
How Aslam HQ is put together
  1. Everyone signs in on one page. Supabase Auth issues the session, and middleware checks it on every request: a session ends after 3 days, or after an hour with no activity.

  2. The app reads and writes data straight from the browser, as the signed-in person. Row-level security in Postgres decides which rows come back, so the browser only ever receives what that person was granted.

  3. Jobs that need a secret go through Next.js routes: managing logins, sending email, posting to LinkedIn and X, and calling Claude. Each route checks the person and their grant again before it uses the service key.

  4. Two Chrome extensions feed the pipeline. One saves a LinkedIn profile or a Sales Navigator lead in one click, and the other reads the LinkedIn inbox from your own open tab. Both sign in as you and write through the same rules.

  5. Email goes out through Resend from a verified domain, with its record written first. Posts go to LinkedIn and X through their official APIs, now or on a schedule.

  6. The AI features read the conversation through the caller's own session, so Claude only sees what that person is allowed to see, and every call is logged with what it cost.

The stack

Front end

  • Next.js 15 (App Router)
  • React 19 and TypeScript
  • Tailwind CSS 4
  • TipTap email editor, dnd-kit boards

Data and sign-in

  • Supabase: Postgres, Auth, Storage
  • Row-level security on all 33 tables
  • Sessions: 1 hour idle, 3 days at most

AI

  • Claude: Sonnet 5.5 writes, Haiku 4.5 summarises
  • One-hour cache for the shared writing rules
  • The cost of every call, priced on the day

Email and social

  • Resend, from your own domain
  • LinkedIn and X official APIs over OAuth
  • Post pictures drawn as SVG, rendered to PNG

Browser extensions

  • Lead Acquisition: save a lead in one click
  • Message Tracker: reads your own inbox
  • Session token only, no password stored

Import and export

  • CSV import of prospect lists
  • CSV export of leads and conversations
  • Duplicate detection on LinkedIn links

The three hardest problems, drawn

Who may see which row

A client, an employee, a teammate and the owner all use the same app and the same tables. A hidden button is not protection, so the real rules live in the database.

Each person has grant rows: which profiles, which clients, which employees, which mailboxes. A row comes back only if a grant exists for it. Middleware and the routes check again, so a request made by hand gets the same answer: nothing.

Three gates and a database that says noA clientsigned inopens their projectasks for someoneelse's project1 Middlewaresession fresh? section granted?2 API routeadmin actions: owner only3 Row-level securityis there a grant row for this record?their project,read onlypasses: signed inpasses: it is a readno grant row for itzero rows,no hintHiding a button is a convenience.The database is the gate.Even a hand-made request gets nothing back.
Three gates and a database that says no

Never send or post twice

Email and social posts are the two things that cannot be undone. A double click, two open tabs or a dropped connection must never send a message or a post twice.

An email is written to the database as a row before it goes to Resend, so a cut-off send still shows up with a Retry. A post is claimed in one atomic step before it is sent, and a network that already took it is never sent to again, so a retry only goes where it failed.

Write it down first. Claim it before you send.Email: the record exists before the sendComposein the editorServer checksmailbox granted?daily limit left?Write the rowstatus: sendingResendaccepts or refusesUpdate the rowdelivered · opened · bouncedSent viewwhat they gotcut off? the row says so, and Retry reuses the keyPosts: one claim, then one send per networkPost readynow or scheduledSchedulercron · pg_cron · open tabClaim itscheduled to publishing,in one atomic stepLinkedIn · Xeach network on its ownResult per networkposted · failed · linka retry sends only where it did not landsecond tab? second scheduler?Claim before you send. A retry is safe.
Write it down first. Claim it before you send.

Many chats, one person

LinkedIn shows the same person under different chats and with different name endings, like a job title or a credential. HQ has to count them once, or every number is wrong.

Each person gets one key from a cleaned-up name, chats with the same key fold into one conversation, and the match back to a lead goes by profile link first, then by name. The plan is shown for review before anything is written, and it never moves a lead out of replied or beyond.

Many chats, one personExample names, not real peopleAmelia Ellis, (MBA)chat from MarchAmelia Ellissecond chat, same personProspect: Amelia Ellislinkedin.com/in/amelia-ellis-demoMake it one person1 clean the name (drop credentials)2 fold chats with one key3 match by profile link first4 then by nameone key per persona planThe plan, for review1 message: stays new2 or more: contactedreplied and beyond:never touchedapply what you approvedThe pipelineleads move to contactedOne person, one row,however many chats.Replied and beyond stayyour call, never a guess.
Many chats, one person

Other problems along the way

Closing a way to become the owner

Owning a profile is what makes someone the owner, and the first version let any signed-in login create one. Anyone could have made themselves an owner. Now only an existing owner adds profiles, and the database enforces it, not just the interface.

Sessions that end on their own

A session ends after three days, or after an hour of no activity, and it is revoked on the server so a direct call to the database loses access too. A background check never counts as activity, so an open tab cannot keep a session alive by itself.

Tokens the browser can never read

LinkedIn and X sign-ins sit in a table with no access rule for logged-in people at all, so only the server can read them. They can also be encrypted, and the browser only ever hears yes or no.

AI that stays cheap and visible

The writing rules and business context are one cached block, so later calls read it back at a fraction of the price, and a rewrite sends only the text being changed. Every call is logged with the price on the day, so a later price change never rewrites history.

LinkedIn's and X's own rules

LinkedIn sign-ins last 60 days and cannot renew, so the page warns a week ahead. X charges more for a post with a link, so the writer is told never to put one in an X post and the composer warns if one slips in.

Changing the database without breaking it

One SQL file, safe to run againSQLblocksschema.sqlone file, many blocksScratch databasewhole file, twice, thesecond time with rows inBoth runs pass?every check lists every valueSupabase editorpaste, runDeploy the appnew code, same dataa failure goes back to the file, never to your dataEvery block is self-contained,so a re-run changes nothing.
One SQL file, safe to run again
  1. The whole schema is one SQL file, written in blocks that are each safe to run again on a database that already holds data.

  2. Before a change ships, the whole file runs twice on a scratch database, the second time with realistic rows already in it.

  3. That second run caught a real bug: a check constraint named only some of the kinds of AI record the app writes, so a re-run failed on live data. Every check now lists every value the app writes.

  4. Then the file goes into Supabase's SQL editor and the app is deployed. Upgrading an older install is the same step.

From the internal dashboard

More from Aslam HQ

Every screen on this page shows invented sample data: made-up names, companies and numbers. The real dashboard holds real clients and LinkedIn results, and it is private.

Keep reading

Next case study