Product and growth engineering
Your product and its data layer come first. The growth engines run on the same database, and first-party events record what brought each signup, so you know what to build next.
Products built with AI
AI builds your app and its Postgres database from specs I write, and your site in the CMS your team edits, and I read every change before it goes live. Your app records what each user does in that database, and the five data systems below read from it.
App built under review
36builds, each read before it went live
Your app is built by AI from written specs, and I read each change before it goes live, so the mistakes AI code makes are caught before a user meets them.
- 1 monthfrom an empty database to production
Each feature starts as a written spec with rules every build must follow: add to what works and never rewrite it, leave sign-in and attribution alone, check each column name against the live database. An AI app builder, driven by a coding agent over MCP, writes the change; the exact change, the diff, comes back to me, and nothing goes live until it reads clean.
On a stealth biotech startup's member app, my review of all 36 builds caught rewrites that would have lost a user's first change, two builds writing to database columns that did not exist, saves using the sign-in ID where the database expected the profile ID, and links between tables that would have blocked account deletion. The app went from an empty database to production in one month, with a 14-field intake, a progress dashboard, a calendar on a unit-tested dosing model, data export and account deletion.
Builds read 0 / 36
One build, read line by line
Log a check-insrc/routes/checkin.tsx
const { data: { user } } = await supabase.auth.getUser() +if (!user) throw new Error("Not signed in") +const { error } = await supabase.from("checkins").insert({ + member_id: user.id, + mood, + note, +}) +if (error) throw error
Held
Each profile has an ID of its own, apart from the sign-in's. member_id takes the profile's ID, so passing user.id makes row-level security reject every check-in.
Added to the build rules: Check each ID against the column it fills
Who does each step
- Lovable's builderwrites the code
- Claude Opus, in Claude Codewrites the specs and drives the builder
- Reviewme, on each change
Stack
- Claude Codewrites the specs and drives the builder
- Lovable, over MCPthe builder
- TanStack StartReact with server-side rendering
- Tailwind CSSthe styling
- shadcn/uithe components
- Supabasethe database
Outputs
- The app in production
- Review recordeach build, its change and what was caught
- Build rulesthe standing rules, extended after each catch
Postgres data layer and its audit
6 of 6audit findings closed, three high and three medium, each with a fix and a check
Row-level security makes your database check who can read and write each row, and a read-only audit tests it before launch, so protecting your users' data does not depend on the app's code alone.
An AI agent writes each change to the database as a numbered script, a migration, and a parser reads each one before it runs, so a script that does not parse never reaches the database. Every table gets row-level security, so the database checks each read and write against the user making it. Functions with extra rights are locked down, a trigger stops members writing to fields reserved for admins, and admin actions are logged. The views shared with partners hide identities and show no figure for a group of fewer than five people.
Before launch I audit the live database read-only and rank what I find. On the stealth biotech startup's database that was three high and three medium findings, each closed with a staged fix script and a check that the fix held.
Every table: row-level security
Who can read and write each table
| Member | Admin | Partner view | |
|---|---|---|---|
| profiles | Own row | Read; changes logged | Hidden |
| checkins | Own rows | Read; changes logged | Groups of five or more |
| events | Add own, never edit | Read | Hashed keys |
| admin_fields | Blocked by a trigger | Write, logged | Hidden |
| admin_log | No access | Read | Hidden |
Who does each step
- Claude Opus, in Claude Codewrites the migrations
- Parsing and the audit's testscode, no model
Stack
- Supabase Postgresthe database
- PL/pgSQLfunctions and triggers
- pglastparses each migration before it runs
- Supabase MCPapplies them
- Supabase advisorsthe audit's first pass
Outputs
- The databasebuilt from versioned migrations
- Auditeach finding, its severity, its fix and its check
- Access mapwho can read and write each table
Sites and web apps
10 daysto rebuild the site for a conference deadline
Your website, built in the CMS your team already edits, or in Next.js on its own database when the site has to work as an app, and moved to your domain with no downtime.
- 8pages
- 31custom React components
When your team edits the site itself, an AI agent builds the pages and templates from your brief, brand and content inside Framer, Webflow or WordPress, through their MCP servers and APIs, and writes the custom parts as React components. When the site has to sign people in, save what they do or build pages from your data, it is a Next.js app on Vercel, with its DNS on Cloudflare and a Postgres database, so the site and your product share one record.
I review each page before it goes live, and the move changes only the DNS record that points your domain at the new host, so your email and the rest of your domain keep running. For a stealth biotech startup I rebuilt the marketing site in Framer in ten days for a conference deadline: 8 pages and 31 custom React components, with no downtime when the domain moved.
Your domain's records
- AYour domainPoints at the new host
- MXMailUnchanged
- TXTSPFUnchanged
- TXTDKIMUnchanged
- TXTDMARCUnchanged
Who does each step
- Claude Opus, in Claude Codebuilds the pages, the components and the app
Stack
- Claude Codethe build
- Framerover MCP, where your team edits
- Webflowthe same, over MCP
- WordPressthe same, over MCP
- Next.jsthe site as an app
- Vercelhosting
- Supabase Postgresthe app's data
- CloudflareDNS and the move to the new host
- GA4analytics
Outputs
- The sitein your CMS or in Next.js, yours to edit
- Componentsreusable on new pages
- Go-live recordthe domain moved with no downtime
Listings from unstructured posts
Messy posts from a Telegram group, an inbox or a form become clean listings (item, price, place, contact): a low-cost model reads each post, a stronger one takes the posts it is unsure of, and a person checks any post still below the confidence level you set.
As each post arrives, a low-cost model reads it into fixed fields (item, price, place, contact) and scores how sure it is. A post it scores below the confidence level you set goes to a stronger model, and one still below that level waits for a person.
After that it is all code: the listing is validated, saved in Postgres and placed on the map or in the feed. Most posts need only the low-cost call, which keeps the cost low enough to read every post your channels receive.
selling my road bike, barely used, 450 or best offer, pick up in Lisbon centre, message me
ItemRoad bikePriceโฌ450PlaceLisbonContactMessageHaikuPlacedstanding desk free if you collect it this week!! porto, near the station, reply here
ItemStanding deskPriceFreePlacePortoContactReply hereHaikuSonnetPlaced2 bedroom flat from 1 december, 1200 a month, message for photos
Item2-bedroom flatPriceโฌ1,200 a monthPlace?ContactMessageHaikuSonnetHeld for a personcamera lens 50mm f1.8, 120, Braga, message
Item50 mm lensPriceโฌ120PlaceBragaContactMessageHaikuPlaced
Listings on the map
Your level 80
Who does each step
- Claude Haikureads each post first
- Claude Sonnetthe posts Haiku is unsure of
Stack
- Supabasethe listings
- Telegram Bot APIthe channels the posts come from
- Mapboxturns each place into a point on the map
Outputs
- Listingsone per post, checked against the fields
- Live map or feedevery listing placed
- Review queuethe posts waiting for a person
Data
These five systems read that database and the sources around it, so each figure you act on, from a signup's first link to a price that moved, traces back to the rows that produced it.
Product analytics
54types of event recorded for a stealth biotech startup
A record of what each user does, kept in your own database from the first link they clicked to what they do as a member, so you know what brought each signup and what to build next.
Your app sends each event to a table that can only be added to, tied to the signed-in user, or before signup to an anonymous ID. That ID links a visitor's visits before signup to the member after it, the source tag on the first link carries into the signup and every event after it, and the app sends events in batches and saves them when the page closes.
Partners and analysts read a version with identities hidden behind hashed keys instead of the raw table, and an attribution view shows what each first link brought, counting a member as active 30 days after signup when they recorded an event in the seven days before. For the stealth biotech startup it tracked 54 types of event.
Events as they are recorded
| Time | Event | Who | First link |
|---|---|---|---|
| 10:41:52 | page_viewed | anon 7f3c | newsletter |
| 10:43:10 | pricing_viewed | anon 7f3c | newsletter |
| 10:44:51 | signup_completed | anon 7f3c, now member 51c2 | newsletter |
| 10:45:30 | onboarding_completed | member 51c2 | newsletter |
| 10:47:02 | project_created | member 51c2 | newsletter |
The anonymous ID joins the member
What each first link brought in August
Active 30 days after signupGone quiet
Who does each step
- Noneevents are recorded, never guessed
Stack
- Supabase Postgresthe events
- Row-level securitywho can write them
- A hashed read viewwhat partners see, identities hidden
Outputs
- Eventsall of them in your own database
- Attributionfrom the first link to each event
- Partner viewidentities hidden behind hashed keys
Email and link tracking
Your email click counts leave out the security scanners that open links automatically, so you judge a campaign on what its readers clicked.
Each email carries a tracking pixel for opens, and each link goes through a redirect that records the click and adds a campaign tag, so GA4 credits the visit to the email. The events are stored in Cloudflare R2.
Company security scanners open the links in an email within seconds of sending, from a data centre. Code spots them by that timing and that network and keeps them out of the counts. Opens stay a rough signal, since some mail apps load every email's pixel on arrival, so a campaign is judged on its clicks.
Sent at 09:00:00
0Clicks by people
0Scanner hits kept out
- 09:00:03ClickData centreScanner: opened within seconds, from a data centre
- 09:00:04OpenData centreScanner: opened within seconds, from a data centre
- 09:00:06ClickData centreScanner: opened within seconds, from a data centre
- 09:14:22OpenHome broadbandPerson
- 09:14:40ClickHome broadbandPerson
- 09:31:05ClickMobilePerson
- 10:02:17ClickOfficePerson
Who does each step
- Nonecounting is code
Stack
- Cloudflare R2stores the events
- GA4credits each visit to its email
- Next.jsrecords the opens and clicks
- Vercelhosting
Outputs
- Human funnelclicks and visits by people, with opens beside them
- Bot logeach scanner hit and why it was flagged
- Reportthe funnel, behind a login
Enrichment and verification
Each record is filled in from public sources, and code marks each field verified, likely or unverified, so nothing downstream treats a guess as a fact.
Code fills in each record you send (a lead, a company, a contact) from public sources and tags each field with its source, and a model decides which records across sources describe the same one. An email address is judged on where it was seen in public, what its mail server answers when asked whether the mailbox exists, and whether it fits the domain's address format, since some servers accept any address.
Each field gets a grade: verified when a primary source confirms it, likely when sources agree, unverified otherwise. Your outreach, your CRM and your reports read only the grade they need, and an address that fails the check stops a send.
- CompanyCompany registryVerified
- WebsiteRegistry and DNSVerified
- RoleTwo public profiles agreeLikely
- CountryOne public profileUnverified
- EmailThe mail server accepts any addressHeld
Who does each step
- Claude Sonnetmatches records across sources
- Verificationcode, no model
Stack
- Pythonruns the pipeline
- Hunterfinds addresses
- SMTP checksask the mail server whether the mailbox exists
- Postgresthe graded records
Outputs
- Graded recordseach field verified, likely or unverified
- Verified contacts
- Source per field
Pricing and market data
Prices and market data from scattered sources land in one table you can question in plain words, and each answer shows the rows it came from.
Each source (supplier feeds, exports, scraped pages, APIs) has its own connector that runs on a schedule. Python converts the units, currencies and names into one format in Postgres, and code works out the price changes by competitor, product and region from it.
You ask a question, such as which competitor cut prices in a region this month, and a model writes the database query, runs it read-only and answers with the rows behind it. A new source means one new connector, and nothing else changes.
- Supplier feed
- Export
- Scraped pages
- API
One price table
select competitor, product, change from price_changes where region = 'North' and month = '2026-10' and change < 0 order by change;
| Competitor | Product | Change |
|---|---|---|
| A national chain | Espresso beans, 1 kg | -8.5% |
| A national chain | Filter beans, 1 kg | -6.0% |
| An online shop | Espresso beans, 1 kg | -3.2% |
The national chain cut two products in the north this month, by 8.5% and 6.0%.
Who does each step
- Claude Sonnetturns your question into a database query
- Conversioncode, no model
Stack
- Pythonthe connectors
- Supabase Postgresthe table
- Apifyscraped sources
Outputs
- One price tableevery source in one format
- Signalsprice changes by competitor, product and region, refreshed
- Answerseach with its query and rows
Reports from your data
A weekly summary of your numbers from your analytics and your database, with each figure tied to the query that produced it.
Queries run on a schedule against GA4, Mixpanel or the events in Postgres. Code works out what changed against the week before and your targets, a model writes it up, and a check rejects any figure in the summary that does not match its query's result.
The summary is sent to you each week, and the charts sit on a dashboard behind a login.
- Signups rose to 312, 9% more than last week.Matches its query
- 58% of September's signups were active 30 days later.Matches its query
- Paid plans brought in $41.2K.Held: does not match its query
Query sheet
- count signups, this week312312
- active at 30 days, September58%58%
- sum of paid invoices, this week$41.2K$40.6K
Who does each step
- Claude Sonnetwrites the summary
- The figurescode, no model; each comes from its query
Stack
- GA4
- Mixpanel
- Postgres
- Next.jsthe dashboard
- Vercelhosting
Outputs
- Weekly summary
- Dashboard behind a login
- Query sheeteach figure with its query
Send me what you're growing.
There is no call to book. Send me an email and I reply in writing.
jose@jcomarketing.io




