Portfolio

Case study · 2026

SmartMeetUp: meetings that write their own notes

A browser-based video meeting app that does the boring part of meetings for you. You call a teammate, talk, and hang up. A few minutes later the meeting has a recording, a transcript that knows who said each line, an AI summary, action items with owners and due dates, decisions, speaking-time analytics and a follow-up email ready to send.

Role
Solo full-stack developer — product, frontend, backend, infrastructure, testing
Timeline
July – October 2026
Stack
Angular 21 · ASP.NET Core 8 · LiveKit (WebRTC) · SignalR · PostgreSQL + pgvector · Hangfire · AssemblyAI · LLMs
SmartMeetUp dashboard on desktop and a live call on a phone

The short version

I designed and built the whole system on my own: a real-time calling experience that behaves like a phone, a background processing pipeline that turns raw audio into structured notes, and the infrastructure to run all of it in production for $0 a month. Every meeting you have ever had becomes searchable by keyword or by meaning.

The problem

Meetings produce decisions and to-dos, and both get lost. Someone has to take notes. Nobody remembers who agreed to what, and a week later the only record is a vague memory. The tools that fix this are usually bolt-ons to someone else's video platform, which makes them expensive and disconnected from the call itself.

  1. Talking should be effortless.Calling a teammate should feel like calling them on a phone: it rings, they answer, you're talking.
  2. The notes should write themselves. No bot to invite and nothing to upload. Hanging up is the trigger.
  3. The output should be trustworthy. A summary is only useful if you can check it against who actually said what, and when.
  4. It should run for free. Every service had to fit inside a free tier without the product feeling cheap.

What it does

1. Calls that behave like a phone

From the dashboard you search for a teammate and press Call. A meeting page opens and shows them being rung. On their side, wherever they are in the app, a popup appears with Accept and Decline, and a ringtone plays.

Incoming call popup over the dashboard
The incoming-call popup follows the user anywhere in the app.

Inside a call, the only way to bring someone in is Add people, which invites them into the same room. Calling a different person means leaving first, the same rule a phone follows. The server enforces this too, not just the UI.

A three-person call with chat open
A three-person call with chat open.
Camera and microphone check before joining
Camera and mic check before joining.

2. Notes that write themselves

When the call ends, the meeting shows as Processingand can't be opened until its notes are ready. Then it unlocks with an AI summary and key topics, a transcript attributed to each speaker, action items with an owner and due date, the decisions made, a drafted follow-up email, and speaking-time analytics.

AI summary of a meeting with topics and participants
Summary, topics and participants, generated after the call.
Transcript with each line attributed to a named speaker
Each line is attributed to a real person, with timestamps.
Action items with owners and due dates
Action items with owners and due dates.
Speaking time per participant
Who spoke, and for how long.

3. A searchable memory of every meeting

Every transcript is split into chunks and indexed twice: once for keywords and once for meaning. Search "onboarding" and you get the exact moments it came up, across every meeting you attended, each linked to its timestamp.

Searching across meeting transcripts
Keyword and semantic results, merged into one ranked list.
Account analytics: time in meetings, meetings per week and recurring topics
Time spent in meetings, meetings per week and the topics that keep coming back.

4. Built for phones as well as desktops

Every screen was designed for a 390px-wide phone as well as a desktop. Navigation becomes a slide-out drawer, tables turn into cards, the video grid stacks tiles vertically, and the chat and people panels slide up as a bottom sheet instead of squeezing the video.

SmartMeetUp running on phones

Architecture

Architecture diagram and the post-meeting processing pipeline

Why an SFU instead of peer-to-peer WebRTC?The first prototype used a peer-to-peer mesh, where each browser sends its video to every other browser. That works for two people, but upload bandwidth grows with every participant, and there is no single stream to record. LiveKit's SFU means each browser uploads once and the server forwards the streams, which made larger calls practical and server-side recording possible.

Engineering deep dives

"Who said that?" Attributing the transcript to real people

Speaker diarisation can tell voices apart, but it labels them anonymously: Speaker A, Speaker B. A summary saying "Speaker B will finish the onboarding screens by Friday" is useless. LiveKit knows who is speaking at each moment, but only reports it live to the browsers in the room, and its server webhooks carry no speaker data at all. So the attribution data had to come from the client: each browser records the intervals when its own user was talking, batches them to the API over SignalR, and a background job assigns each diarised label to the person whose speaking time overlaps it most.

The bug that taught me the most.In production, one participant's lines sometimes stayed "Speaker B" for an entire call. The API checked membership against in-memory presence, which is rebuilt whenever a connection drops and reconnects; during that window the server believed the user was in no meeting and silently discarded their speaking intervals. Checking the meeting's participant record in the database instead fixed it, and the client now retries unsent intervals rather than dropping them.

A pipeline that never leaves a meeting stuck

Turning a call into notes takes several external services, each of which can be slow or fail. The pipeline runs as a chain of Hangfire jobs driven by LiveKit webhooks: Scheduled → Live → Processing → Ready (or Failed, with a retry button).

LLM analysis on a free tier

The analysis step asks an LLM for a strict JSON document: summary, topics, action items with assignees and dates, and decisions. Free tiers are unpredictable, so a provider registry covers Gemini and any OpenAI-compatible API, chosen through config rather than a deploy. If a provider says "retry in 8 seconds", the job waits; if it says "retry in 9 hours", it moves straight to the next provider. Transcripts are trimmed to each model's context window, and replies are parsed and validated, so a malformed answer counts as a failure instead of being written to the database.

A production bug with pgvector

Shortly after the first deployment, saving search embeddings failed in production with a type-mapping error, though it had worked locally. I reproduced it in an isolated test project: Hangfire opened the first Npgsql connection before EF Core had registered the vector type, and Npgsql shares type mappings across connections from the same source. The fix was to give EF Core its own dedicated data source with the vector extension enabled.

Search: keywords and meaning

Transcripts are chunked by utterance, not by raw text length, so every result can jump to that moment in the meeting and chunks never split mid-sentence. Each chunk is stored with a PostgreSQL tsvectorfor keyword ranking and a 768-dimension pgvector embedding for semantic similarity. The two lists can't be blended by score, because ts_rank and cosine distance use completely different scales, so they are merged with Reciprocal Rank Fusion, which combines results by rank instead.

Calling that feels like a real phone

Shipping it for $0 a month

ConcernServiceWhy
FrontendVercelStatic hosting with a global CDN
APIRender (Docker)Free web service, health checks, easy env config
DatabaseNeon PostgreSQLServerless Postgres with pgvector built in
RecordingsCloudflare R2S3-compatible, no egress fees, lifecycle rules
MediaLiveKit CloudManaged SFU and recording on the free plan
TranscriptionAssemblyAISpeaker diarisation included
AIOpenRouter, GeminiSeveral providers for resilience

The trade-off is cold starts: Render's free tier sleeps the API when idle, so the first request after a quiet period is slow. Production hardening also covered per-IP rate limiting on authentication and other costly endpoints, structured logging with Serilog, health endpoints, and secrets in environment variables only.

Quality and testing

~8,500 lines of C#~11,000 lines of Angular~3,000 lines of tests38 REST endpoints9 migrations

What I learned

What's next

Horizontal scaling with a Redis-backed presence store and a SignalR backplane, a light theme on the existing design tokens, push notifications so a call can ring with the app closed, and live captions during the call rather than only after it.

See it running

The demo is seeded with example meetings, so you can open a summary and search transcripts right away.

Designed and built end to end by Muhammad Amjad. Back to the portfolio →