I Looked at My AI Companion App's Retention Data. The Cliff Wasn't Where I Thought.
The Dashboard Was Lying to Me
Before any of the numbers, the embarrassing part.
I have an admin dashboard for Tendera that shows retention, a message leaderboard, unique visitors, referrers. I'd been reading it for weeks. It was wrong.
Supabase — which is to say PostgREST underneath it — enforces a server-side cap on how many rows a single read returns. On this project that cap is 1000. Here's the part that got me: `.limit(50000)` does not override it. The response is silently clamped to 1000 rows. No error, no warning, no truncation flag. Whichever `.order()` you used decides which 1000 rows you get.
There was even a comment in my own code claiming a previous fix had solved this. It hadn't. It had added a bigger `.limit()`, which does nothing.
So once the messages table passed a thousand rows, every "read the whole table" query in that dashboard was quietly reading the newest thousand and calling it everything. Retention, the leaderboard, total visitors, the device split, the referrer breakdown — all computed on a truncated slice. The numbers looked plausible. They were fiction.
The fix is unglamorous: paginate with `.range(from, from + 999)` in a loop until a page comes back short. Don't trust `.limit()` for completeness. If you're building on Supabase and you have any query whose result you treat as "all the rows," go check it right now. This is the kind of bug that doesn't announce itself — it just quietly makes you wrong about your own product.
The reason I'm leading with this: everything below only exists because the real numbers turned out to be a different story than the fake ones.
The Real Numbers
Measured across all history, after the fix:
Two caveats before anyone quotes these at me. The cohorts are small — this is a young product, and these are rates over tens of people, not thousands. Treat them as directional. And they're anchored on a user's first message, not their signup date, so "activation" and "retention" are measuring different populations on purpose.
The truncated dashboard, for what it's worth, had been showing me an activation rate around 38% and a D30 of zero. Both were artifacts. The newest thousand messages simply didn't contain the early heavy users, so the people who'd been around longest were invisible to my own analytics.
The Shape Is the Finding, Not the Numbers
Look at those three retention figures again: 15 → 13 → 12.
The interesting thing isn't the absolute level. It's that the curve flattens. Almost everything you lose, you lose on day one. From week one to month one, the line is nearly horizontal.
That's a meaningfully different diagnosis than "people churn." If the curve kept sliding — 15, then 8, then 3 — the story would be that nothing holds, the product slowly fails everyone, and there's no core. A flat tail says the opposite: there is a group who genuinely stay, and the losses are concentrated in a single place.
For a solo founder staring at a small user base, this distinction is the difference between despair and a work item. "Nobody sticks" is a reason to quit. "Everybody who survives the first day sticks, and most people don't survive the first day" is a bug report.
So Where's the Leak?
The first 24 hours, and specifically the first session.
Of activated users, roughly four in five are one-and-done: a single session, well under an hour, a handful of messages, never seen again. The people who stayed look completely different — many active days, hundreds of messages, conversations spread across weeks.
There isn't a gentle slope between those two groups. There's a canyon. Almost nobody has a medium relationship with this product. You either bounce in twenty minutes or you're still here a month later.
The Signal I Almost Misread
Here's the pattern that jumped out, and the trap inside it.
Users who stuck had touched far more characters than one-and-done users — roughly three and a half on average versus a bit over one. The correlation is not subtle.
The tempting conclusion: exposure drives retention. Get new users to sample more characters and D1 goes up. I nearly built the roadmap on that.
It's almost certainly backwards.
The likelier story is reverse causation: people who are already engaged go exploring. Exploration is what engagement looks like, not what produces it. Herding a disengaged user through four character profiles will not make them engaged — it'll make them close the tab from a different page.
Which means the real lever isn't "more characters." It's the thing the correlation is a shadow of: helping someone find the right character faster, and making the first conversation good enough that they want a second one. Those are different interventions, and only one of them is honest about what the data actually says.
There's a positioning trap here too. The obvious way to "increase exposure" is to build a browsable feed of characters — which is precisely the product I'm trying not to build. Tendera exists to let you meet one specific written person, not to hand you a carousel. A change that lifts a metric while dissolving the thing that makes the product itself would be a bad trade, and metrics are very good at talking you into bad trades.
What I Actually Changed
Three things, all aimed at the first session rather than the funnel above it.
A matchmaker instead of a default. There was already a personality quiz that routes you to whichever of the four characters actually fits you. It was buried below the fold, under the character grid — so new visitors scrolled past it, or never got there, and defaulted into whoever was featured. I moved the entry point up into the hero, above the character chooser, and cut a redundant intro screen so the quiz opens directly on the first question. Fewer clicks between "I don't know who to talk to" and "oh, her."
Carrying the conversation through signup. This one still bothers me. Guests can chat five messages before hitting a wall. When they signed up at that wall, the app used to send them to a hardcoded character with an empty chat — throwing away both the person they'd just been talking to and the conversation they were in the middle of. At exactly the moment their momentum was highest. Now signup returns you to the character you were talking to, with the preview conversation migrated into your account, and you keep going mid-thread.
Tighter re-engagement email. The recall email that goes out after someone goes quiet was fine but flabby. Sharper subject lines, two lines of body instead of four, one clear invitation.
What I Still Don't Know
Whether any of it works.
That's not modesty, it's arithmetic. At a few signups a day, a statistically valid A/B test on retention would take months — and the product will have changed three times underneath the experiment before it concludes. Small products don't get to run experiments; they get to reason from mechanism, ship the most defensible version, and watch the line directionally over weeks.
So I've picked the interventions I can defend on their logic — remove friction at the moment of highest intent, don't discard someone's momentum, help them meet the right person faster — and I'll watch. If D1 moves, I won't be able to prove which change did it. I've made peace with that.
The Part Worth Stealing
Two things I'd hand to any other builder looking at their own numbers.
Check that your dashboard isn't lying. Any query whose result you treat as "the whole table" is a candidate. Row caps, silent truncation, a `.limit()` you assumed was doing something — these fail quietly and make you confidently wrong. I made product decisions off bad data for weeks and didn't know.
Read the shape before you read the level. A flattening curve and a sliding curve call for completely different work, even at identical D30. Flat means you have a core and one leak. Sliding means you have a hole in the boat. I spent a while thinking I had the second problem. I had the first one — and the first one you can actually fix.
---
If you want to see the thing all this is in service of, Tendera is four written characters you can talk to without signing up — five messages, no email, no card. The whole bet is that a specific written person beats a configurable one, and the retention data is how I find out whether that's true.
Ready to meet your AI companion?
Four unique personalities. Each one remembers you. Free to start.
Meet Your MatchKeep Reading
What AI Companion Memory Actually Does — and Why It Can Still Feel Forgetful
AI companion memory is more than storing facts, but less than perfect human recall. Here is how recent context, structured memory, character writing, and judgment work together.
How to Write an AI Character Backstory That Changes the Conversation
Most AI character backstories are lists of facts. Learn how to write specific causes, contradictions, relationships, and behavioral details that actually shape an AI character's replies.
Why Your AI Companion Should Disagree With You Sometimes
AI companions are very good at making users feel understood. New research suggests constant agreement has a cost. Here is why a believable character needs judgment, boundaries, and a way to say no.