
Click CRM
An all-in-one agency CRM that ships with funnels, pipelines, payments, marketing, and per-client Stripe Connect. Built entirely through conversation with MeDo's AI agent, with nearly 6,000 credits burned over the build.
Inspiration
I've spent way too many late nights stitching together GoHighLevel, Notion, Stripe, and a clunky email tool for small marketing agencies, and the same workflow keeps repeating. An agency takes on a client (a "sub-account"), spins up a funnel, wires up a pipeline, hooks Stripe, sends a few campaigns, and prays nothing leaks across tenants. Every agency I've watched does this manually, and every one of them eventually screws up RLS or accidentally charges client A's card on client B's product.
When I saw MeDo's hackathon I realized I had a rare combination: a vibe-coding platform with a built-in Supabase backend, a deep-build AI agent that can actually reason about schemas and edge functions, and a plugin system that turns external APIs into first-class skills. That's exactly the surface area a CRM lives on. I decided to build the platform I always wished existed, and use MeDo to do it in a week instead of a quarter.
What it does
Click CRM is a multi-tenant CRM for marketing agencies. One login, three layers:
- Agency layer. Billing, sub-account list, members, invitations, and a dashboard with rolled-up KPIs and a "top performers" board.
- Sub-account layer. One per client. Has its own dashboard, pipelines, contacts, funnels, marketing studio, settings, and Stripe Connect status.
- Funnel layer. Every sub-account can publish funnels on its own subdomain at
/sites/:subDomain/*, with drag-and-drop pages, AI-generated layouts, contact forms, and embedded payment forms wired to that sub-account's Stripe.
The feature surface:
| Area | What it does |
|---|---|
| Agency dashboard | KPI cards (MRR, active subaccounts, pipeline value), traffic, top performers, recent activity |
| Sub-account dashboard | Funnel-level conversions, checkout activity chart, Stripe transactions table, "connect Stripe" overlay if not configured |
| Pipelines | Full Kanban with @dnd-kit. Lanes, tickets, tags, assignees, contact linking, drag-to-reorder lanes and tickets |
| Contacts | Tagged contacts per sub-account, auto-created from funnel form submissions |
| Funnel editor | Recursive element tree (__body__ → sections → containers → leaves), undo/redo, device preview, layers panel, properties panel, template picker, "Generate with AI" that streams a full landing-page JSON from Gemini 2.5 Flash |
| Public funnel renderer | Server-aware route at /sites/:subDomain/* that resolves a subdomain to a funnel to a page tree and renders it with the same recursive components |
| Stripe Connect | Each sub-account pastes its own publishable + secret keys; we verify them with Stripe, then store them in Doppler (not Supabase) under a namespaced secret per sub-account |
| Payments | Public PaymentForm clicks generate Stripe Payment Links on the connected account; URLs are cached on the funnel's live_products JSON so checkout is one redirect away |
| Marketing studio | Kling-powered AI model studio. Design a "model" (gender, body type, skin tone, vibe), generate persistent portraits, then spin up branded social posts with format/goal/lighting/props controls |
| "Send email" panel that ships transactional messages via Resend, scoped to a sub-account's contact list | |
| Storage | UploadThing-backed media library per sub-account with previews, rename, delete |
| Roles & RLS | AGENCY_OWNER / AGENCY_ADMIN / SUBACCOUNT_USER / SUBACCOUNT_GUEST enforced both in UI guards and Postgres RLS policies + RPCs |
| Agency billing | Stripe-checkout-based plan upgrades for the agency itself, with a verify-and-mark-active edge function |
Architecture (High Level)
How I built it
Stack: Vite + React 18 + TypeScript + Tailwind, Supabase (Postgres + Auth + Edge Functions + Storage), @dnd-kit for everything draggable, react-hook-form + zod for forms, motion for animation, recharts for dashboards.
The flow with MeDo:
- Schema first. I started every major area by asking MeDo's deep-build mode to draft the SQL migration and the RLS policies in one shot. That gave me a contract. Once the schema was stable, every later prompt could reference table and column names by hand and the agent would stay on rails. The repo has 32 migrations because I treated migrations as the source of truth and let MeDo amend forward rather than mutate existing ones.
- Pages → components → polish. I'd ask MeDo to scaffold a page end-to-end (services, hooks, layout, empty/loading/error states), then peel off complex pieces (the funnel editor, the Kanban board, the marketing studio) into their own conversations so the context stayed surgical.
- Plugins as the integration boundary. Every third-party service (Stripe, Doppler, Resend, UploadThing, Kling) became either a MeDo plugin or a Supabase Edge Function. The UI never talks to those APIs directly. It always goes through an edge function, which means RLS, auth check, and key resolution happen in one trusted place.
- Visual editor for the last mile. The dashboard, landing page, and pipeline board got iterated in MeDo's visual editor. Point at a card, say "make this denser, swap to a 12-col grid on lg, add a sparkline." Way faster than typing prompts for layout tweaks.
Challenges I ran into
- Multi-tenant Stripe key storage. Storing per-sub-account Stripe secret keys in Postgres felt wrong. One RLS slip and every client's keys leak. I burned a full day designing this until I leaned on the Doppler plugin. Each sub-account's keys live under a namespaced secret (
STRIPE_SECRET_KEY_<subAccountId>) inside a single Doppler config. The edge function pulls them just-in-time per request. Supabase only storesstripe_connectedand a last4. Worth every hour. - Drag-and-drop ordering with optimistic UI. The pipeline board has nested sortable contexts (lanes are sortable horizontally, tickets are sortable inside lanes and across lanes). Getting
closestCornersplus cross-lane drop indicators plus aDragOverlaythat doesn't flicker mid-drop took a few rounds. Deep mode was the hero here. I dumped the full reducer and the@dnd-kitevent handlers in one prompt and it caught a subtle bug wherearrayMovewas being called against a stale lane reference. - Streaming AI JSON without breaking the editor. The "Generate with AI" feature streams from Gemini 2.5 Flash through a Supabase Edge Function (
supabase/functions/generate-funnel-page/index.ts). The model loves to wrap JSON in markdown fences and occasionally hallucinates a leaf element withcontent: [...]instead ofcontent: {...}. I solved it with a fence-stripping pass and a recursiveEditorElementvalidator that auto-corrects leaves before swapping the tree. Took two attempts. Deep mode nailed it on the second pass after I pasted the editor'sEditorElementdiscriminated union as context. - Platform jank. Going to be honest, MeDo had bumpy moments mid-build. The publish pipeline failed silently a couple of times, the site was down for an hour or two on one of my late-night sessions, and I hit a few "deploy stuck on building" loops. The flip side: the support team was actually responsive, and the MeDo Discord was a goldmine. Folks shared workarounds for the publish bug before official fixes shipped. Fast-build mode was great for component shuffling, but anytime I had a tricky problem (state machines, recursive trees, RLS interactions), deep-build mode pulled it off even when I knew the prompt was underspecified.
- Idempotent migrations on a rolling DB. A few migrations broke because Postgres state drifted between local and prod (legacy
adminrole values, missinginvitation_sub_account_idscolumns). I added00031_repair_invitation_sub_account_ids.sqland00032_repair_agency_member_visibility.sql, idempotent repair migrations thatIF EXISTS/IF NOT EXISTSevery operation. Lesson learned: every migration should be safe to run twice. - Public site routing under a single domain. I wanted real subdomains per funnel but the hackathon environment serves one host, so
/sites/:subDomain/*became the contract. ThePublicSiteRendererparses the path, resolves the funnel and page, and renders the same recursive components that the editor uses. Same tree, same code, two contexts.
Accomplishments that I am proud of
- One editor tree, two render targets. The recursive
EditorElementmodel powers both the in-app drag-and-drop editor and the public funnel renderer. No separate "preview" code path, no drift. - ~6,000 credits, a real product. ~42k lines of TypeScript, 28 pages, 9 edge functions, 32 SQL migrations, 8 plugins wired end-to-end. Not a prototype. A working agency tool you could put a client on tomorrow.
- Per-tenant secret isolation done right. Doppler-namespaced keys per sub-account is an architecture I'd ship to a real customer. No plaintext keys in Supabase, no shared "platform" Stripe account. Each sub-account gets its own Stripe Connect surface.
- AI page gen that respects the existing canvas. "Generate with AI" doesn't replace your work without asking. It streams, validates, and shows a "replace existing content?" confirm if the canvas already has elements.
- Role-aware everything. The same
useAuth().profile.rolegate that hides the "Members" link in the sidebar is mirrored by an RLS policy that returns zero rows if you bypass the UI. Defense in depth, not just UX gating.
What I learned
- Schemas are the prompt. Once the migration was clean, every subsequent MeDo prompt got 3× more useful. I'd paste 5 lines of
CREATE TABLEinto the prompt and the agent would produce idiomatic Supabase clients, types, and RLS in one shot. - Plugins compose better than scripts. Wiring Doppler with Stripe through MeDo's plugin model meant the edge function I wrote stayed around 100 lines because the plugin handled token sanitization, error shapes, and pagination. I'd reach for plugins first next time.
- Deep mode beats more prompts. I caught myself trying to break a hard problem into many small prompts in fast mode. It was almost always faster to write one good deep-mode prompt with the file paths, the failing trace, and the expected output.
@dnd-kitrewards thinking in regions, not items. The board got dramatically simpler once I modeled lanes and tickets as two separateSortableContexts with explicitdatapayloads.- RLS is a UI feature. Real-time multi-tenant apps where one user's actions are instantly invisible to another tenant feel magical, and that's RLS doing the work, not me.
What's next for Click CRM
- Automations & triggers. The schema is already in place (
automation,action,trigger,automation_instance). Next is a visual builder: "when a contact submits funnel X, create ticket in lane Y, send Resend email Z." - Custom domains per funnel. Today funnels live at
/sites/:subDomain/*. Next: a CNAME flow that lets agencies mappricing.client.comto a funnel. - Sub-account marketplace. Let agencies publish funnel templates to other agencies inside Click CRM. The recursive
EditorElementmodel is ready to be serialized and re-imported. - AI assistant per sub-account. Use the LLM plugin to give every sub-account a chat surface scoped to its own contacts, pipelines, and Stripe data. "Which lane has stalled the most this week?"
- Mobile companion. The agency-owner-on-the-go view: see today's checkouts, drag a ticket to "won," reply to a contact from a push.
- Self-serve onboarding. Today an agency owner creates everything by hand. Next: a guided 5-minute setup that provisions a sample sub-account, a sample pipeline, a sample funnel, and prompts Stripe Connect.
What problem your app solves and why you built it
Marketing agencies juggle 5 to 20 small clients at once, and every client needs the same things: a landing page, a pipeline to track deals, a way to take payments, an email channel, and tight isolation from every other client. The existing options are either monolithic (GoHighLevel, powerful but expensive and opinionated), DIY (Notion + Stripe + Mailchimp + Webflow, duct tape), or enterprise (HubSpot, overkill).
I built Click CRM because I wanted to prove that with MeDo's AI agent, plugin model, and built-in Supabase backend, a single developer could ship the "best of GoHighLevel" in a hackathon week. With better multi-tenant security than most production CRMs (per-sub-account Stripe keys live in Doppler, not the database) and a faster funnel-building surface (drag-and-drop plus AI page generation that respects an existing canvas).
How I structured conversations with MeDo to build my project
A few techniques that punched above their weight:
- "Schema-as-context" prompting. Before asking for any UI, I'd open the relevant migration file, copy the
CREATE TABLEand the RLS policies into the prompt, and then ask for the page. The agent stopped inventing column names overnight. - Screenshot-driven design. For the agency dashboard and pipeline board, I dropped screenshots of GoHighLevel and Linear's board into the prompt with one sentence: "match this density, our brand colors." Visual references beat 500 words of layout description.
- "Show me the failing diff" loops. When something broke, I'd paste the failing TS error plus the file and line number in the prompt. Deep mode would produce a minimal patch instead of rewriting the file.
- Discriminated-union priming. For the funnel editor, I pasted the
EditorElementunion and thehasChildrentype guard at the top of every editor-related prompt. The agent treated those as a hard contract and stopped producing leaves with arrays ascontent. - "One feature, one chat" hygiene. I kept the funnel editor, the Kanban board, the marketing studio, and the Stripe/Doppler integration in separate conversations. Context bleed was the #1 source of regressions early on. Isolating chats by feature fixed it.
- Visual editor for the last 10%. Once a page was structurally right, I'd switch to the visual editor and click-tweak typography, spacing, and gradients instead of round-tripping prompts. Massive time saver.
The most impressive feature MeDo helped me create
The AI funnel page generator wired into the recursive drag-and-drop editor.
Here's why this was the hardest part of the codebase. The funnel editor uses a recursive discriminated union (EditorElement) where containers carry content: EditorElement[] and leaves carry content: { innerHtml } or { src, alt } etc. The reducer (src/components/subaccount/funnels/editor/EditorProvider.tsx) supports undo/redo with a 50-step history, optimistic style updates, cross-parent moves, and a "mark saved" comparison for unsaved-changes warnings. That state machine alone is non-trivial.
Then I asked MeDo to bolt generative AI onto it. The constraints:
- The output had to be a complete, well-formed
EditorElementtree (60 to 120 nodes). - It had to stream so the user sees progress, but only swap into the editor once fully valid.
- It had to not silently destroy whatever the user already had on the canvas.
- It had to share the same recursive renderer that powers the live preview and the public site.
Deep-build mode landed it in two iterations:
- A Supabase Edge Function (
supabase/functions/generate-funnel-page/index.ts) that talks to Gemini 2.5 Flash with a concise schema-aware system prompt, streams SSE frames back, and accumulates them. - A dialog (
src/components/subaccount/funnels/editor/panels/GenerateWithAIDialog.tsx) that handles abort, validation, "replace existing content?" confirmation, and dispatches a singleREPLACE_TREEreducer action so undo/redo just works for the AI-generated page.
The result: type "a fitness coaching landing page with before/after testimonials and a booking CTA" and get back a real, edit-ready 80-node page on the same canvas you were just dragging things around on. And if you don't like it, one Ctrl+Z.
How I used plugins or API integrations to extend functionality
Click CRM ships with 8 plugins, each wired through a Supabase Edge Function so the browser never holds a secret:
| Plugin | What it powers in Click CRM |
|---|---|
| Stripe Payments (MeDo official) | Agency-level Stripe checkout for plan upgrades, plus the platform Stripe Connect entry-point |
| Doppler Secrets Management (custom, parsed from API docs) | Per-sub-account Stripe secret storage. Each sub-account's secret and publishable keys live under a namespaced secret in one Doppler config, pulled just-in-time by the doppler-manage-keys and stripe-operations edge functions |
| UploadThing (custom) | Media library per sub-account. Uploads, listing, deletes, signed URLs for the funnel editor's image picker |
Resend (custom, via skill.md) |
Transactional email sends from the "Send email" panel, scoped to the active sub-account's contacts |
| Large Language Model (MeDo official) | Powers the "Generate with AI" funnel page generator end-to-end |
| Image Generation (Nano Banana Pro, MeDo official) | Used for ad-hoc image generation in the marketing studio sidebar |
| Image Generation (Kling) (MeDo official) | The full AI Models studio in the marketing page. Generates persistent portraits and branded social posts via kling-create-task + kling-query-task async polling pattern |
| PDF (MeDo official) | Powers PDF export of agency reports and Stripe transaction summaries from the dashboard |
The key architectural decision: every plugin call goes through an edge function that re-validates the caller's auth and sub-account membership. The UI passes a sub-account ID; the edge function does supabase.auth.getUser(), checks the user is permitted on that sub-account, then resolves the secret from Doppler and calls Stripe/Kling/Resend/etc. on the user's behalf. That's how a hackathon project ends up with better tenant isolation than CRMs I've actually been billed for.
Click CRM is live, multi-tenant, and powered end-to-end by what MeDo gave me: a real backend, a deep-build agent that could reason about recursive state machines, a visual editor for the polish pass, and a plugin marketplace that turned "I need to integrate Doppler" into "I'll parse Doppler's API docs and have a typed skill in 60 seconds." That's the pitch. Nearly 6,000 credits in, and I'd burn them again.

Built With
- medo
Log in or sign up for Devpost to join the conversation.