I worked on UpLinked for a year and a half: a carousel editor and analytics inside their Bubble app, then the rebuild on Convex with seven AI agents, a strategy planner and a phone app.
UpLinked is a LinkedIn marketing manager for entrepreneurs who are good at their trade and would rather not write about it. It works out a content strategy, interviews you about your week, writes the posts in your voice, makes the visuals, publishes them and tells you afterwards what worked. Bart Gommers and Thierry Crolla started it on Bubble, and I worked with them from spring 2025 until September 2026.
That makes it three products, really. First I built the pieces Bubble could not do, a carousel editor and an analytics dashboard, and embedded them in the Bubble app. Then a 2.0 prototype on Convex that ran inside Bubble. Then the whole thing again from the schema up: a React app on the web, on iOS and on Android, in front of 181 Convex tables, 27 cron jobs and seven AI agents, in Dutch and English. Bart built most of the front end of that himself, Thierry worked on the agents and the interview, Daan on the front end and analytics. I did the backend, the agents, the prompt system, the migration and the billing.
A carousel editor inside Bubble
Carousels, the swipeable PDFs on LinkedIn, were the thing users wanted most and the thing Bubble could do least. So I built them an editor, Canva for LinkedIn basically: text, images, SVG shapes you can recolour, backgrounds and gradients across all slides, layers, alignment, snapping, templates, undo, and a brand kit that pulled your name, photo, headline and colours from your profile. Slides were square or 4:5, and out came a PDF or a zip of PNGs. Bethmi Jayamila and Umanga Wijesuriya built a first version in the DOM; I rewrote the canvas on Konva and took it from there.
It ran at its own address inside the Bubble page, and saved every change as JSON on the carousel record in Bubble through a small server-side proxy, so the API key never reached the browser. Three things took real work. Images uploaded to Bubble came from a CDN without the right headers, and one such image taints a canvas so nothing can be exported; the editor rewrites those addresses to the storage behind the CDN and loads them anonymously. Konva does not fit text to a box, so a binary search measures the text in a hidden element, twenty steps between 8 and 300 pixels. And a PDF made in the browser depends on the browser, so the export runs headless Chromium on the server, which opens the editor itself in an export mode and calls the same export function the button does. The PDF is what you saw on screen.
It was a very cool thing to build and the result was pretty great. Then image models got good, and in the rebuild carousels are generated: the visual agent asks Gemini for one image holding a grid of four or eight slides, the server upscales it, cuts it up and puts the slides in a PDF. Nobody drags a text box anymore.
Analytics went the same way. LinkedIn gives a personal profile no API for its numbers, but it lets you download them as an Excel file, so the dashboard read that file and drew engagement, followers, demographics and the best time to post. Bubble loaded it with one HTML block that fetched a script, and the frame reported its own height back so it sat in the page like any other element.
From a strategy to this week’s posts
In the rebuild, everything starts with a strategy. Twelve settings, things like your ticket size, how mature your market is and how comfortable you are selling, go through a fixed seven-step calculation that gives a sales cycle of two to twelve weeks, a mix of post types and a split between reaching new people and converting the ones who know you. The language model only names and describes what the sum decided. Onboarding has the first week ready in about fifteen seconds, while the setup animation plays, and writes the other eleven weeks in the background.
The strategy agent changes the calendar by sending operations, insert, update or delete, which are applied in one go with an id each, so a retried message does not add a post twice, and the whole change is one step in an undo history the team shares. It sees a month back and five ahead, the same window as the calendar, and only the latest snapshot of the calendar stays in the conversation, which keeps the prompt cacheable and cut the input by about a quarter.
Then it is Monday. You pick this week’s posts, the agent interviews you about each one until it has enough, and three models write three versions at the same time, Claude, GPT and Gemini, so you choose between real alternatives and we learn which model wins. The interview runs in the background and streams into a saved thread, so closing the tab loses nothing; a watchdog ends a stuck answer after ninety seconds and offers to try again.
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ STRATEGY │ │ THIS WEEK │ │ LINKEDIN │
│ 12 settings, a fixed │ │ pick the slots │ │ one scheduled job │
│ 7-step calculation, │──►│ an interview a slot │──►│ per post │
│ a 12-week plan over │ │ 3 models, 3 versions │ │ metrics every night │
│ 18 post types │ │ refine and visuals │ │ and the Excel export │
└──────────▲───────────┘ └──────────▲───────────┘ └──────────┬───────────┘
│ │ │
└──── how posts performed ─┼──────────────────────────┘
│
┌─────────────────┴──────────────────┐
│ INTELLIGENCE │
│ 98 elements, each derived from │
│ data points, read by every agent │
└────────────────────────────────────┘The loop. Performance goes back into the strategy agent, and everything the product learns about you sits underneath, in every prompt.
What it knows about you
The posts only sound like you if the product knows you. Intelligence is 98 elements in five groups: who you are and your business, your message, how you write, what you have to show, and your guardrails, like whether your family may be mentioned. Nobody writes an element directly. Everything that comes in, a pasted document, an answer in the interview, something you said while refining a post, first becomes a data point, and fifteen minutes after the last change a model works out the elements from the data points again, each with a confidence and a link back to where it came from. So you can always see why the product thinks you write like that, and two things you said that contradict each other show up as a conflict instead of the newest one quietly winning.
The Refine agent has one tool of its own, to save an observation. Tell it “no em dashes, ever” and it proposes that as a card; approve it and it is a data point, and from then on every agent reads it. Data points go stale after ninety days.
Pictures, numbers and a reason to come back
The visual agent makes images, carousels, infographics and text carousels, and edits your own photos. Images are Gemini 3 Pro, called directly rather than through OpenRouter, because OpenRouter quietly returned them at half the size we asked for; it stays as the fallback when Google is rate limiting. Writing is free, pictures cost credits, a hundred a month on a paid plan.
For analytics a job runs every night just after midnight, fetching the public numbers of everyone’s posts from a third-party LinkedIn data API, and the Excel export still adds what only LinkedIn knows, like impressions. A post scraped from LinkedIn does not carry our id, so it is matched to its draft by an exact hash first and a MinHash similarity after, because people edit a post on LinkedIn after it went out. Top posts are ranked by how far they are above your own median; likes alone mostly measure how big your network is.
Every Monday there are a few tasks, upload your analytics, answer a quiz, check your strategy, and finishing them keeps a weekly streak going.
Prompts that change without a deploy
Seven agents and a handful of one-shot generators make for a lot of prompts, and the people who know what a good LinkedIn post is are not the people who deploy code. So every prompt, tool description, model choice and post template lives in a second Convex deployment with drafts, versions and rollback, and a change reaches the app within a minute. Tool arguments stay in code, because the code depends on them; their descriptions do not. Every session is one trace in Langfuse, and a thumbs down is stored against the prompt version that earned it.
Moving everyone across
Bubble returns fifty items per API call, so the data came out as seventeen CSV exports and went in as small per-profile jobs, each linked to its old Bubble id and safe to run twice. My first go loaded it in one piece and ran out of memory. Users came back through a reactivation page and found their profiles, posts and subscription where they had left them, at the address the Bubble app used to have. The first days showed what the import had dropped, some knowledge among it, and the same map brought that across.
In your pocket
The same React app is a PWA and, wrapped in Capacitor, an app for iOS and Android with push sent straight to Apple and Google and a widget on the home screen. Apple wants its own payment for subscriptions sold in an app, so the app has no billing at all; you subscribe on the web. The microphone streams to Deepgram for live transcription with a short-lived key, and no audio is kept. Logging in with LinkedIn on the phone never puts the code in the app link: the server holds it behind a one-time handoff that expires in two minutes.