Inspiration

Every link-in-bio tool solves the same narrow problem: one link that fans out into many links. But a stack of buttons doesn't say much about who you actually are. I wanted a single page that could hold the things that actually represent someone — not just "here's my Instagram," but your work experience, your projects, what you offer, how to reach you — styled well enough that you'd actually want to share it as your page, not just a utility link.

What it does

Vexylt is a link-in-bio app built around six content block types instead of one: Link, About, Experience, Project, Service, and Contact. You pick from five hand-designed themes, arrange your blocks, and get a real public profile page with honest view/click analytics — no fabricated charts, just the numbers that actually exist. Free users get the full core experience; Vexylt Pro unlocks unlimited blocks, premium themes, advanced analytics, and removes Vexylt's own branding — one entitlement, wired end-to-end through RevenueCat.

How we built it

The app is Kotlin Multiplatform: one shared codebase for business logic, Firestore access, and UI across Android and a web dashboard (Compose for Web). The public-facing profile page, though, is a separate server-rendered Astro app — Compose-for-Web can't produce the server-rendered OG/meta tags a shared profile link needs to preview properly in iMessage, Slack, or Twitter, so that one page lives outside the shared Compose UI on purpose.

Monetization runs on RevenueCat: a SubscriptionRepository abstraction backed by the native SDK on Android, with a webhook that mirrors each entitlement change into Firestore (users/{uid}.subscription.isPro). That one mirrored field is what lets the web dashboard and the public profile page (both outside RevenueCat's own SDK support) still know whether a given user is Pro, without ever letting a client read RevenueCat's secret key directly.

Challenges we ran into

  • Kotlin/Wasm has no real Firebase SDK. The web target talks to Firestore and Identity Toolkit over hand-rolled REST calls instead of a proper client library.
  • A silent, real bug: Firestore's actual default theme value is the literal string "default", not "light". Code that assumed "light" as the fallback quietly built a hero-image path that didn't exist — and a missing CSS background-image fails with zero visible error, so most real profiles were silently missing their hero photo for a while.
  • Google Cloud org policy friction: iam.disableServiceAccountKeyCreation blocked generating the service-account key RevenueCat needs to validate Play purchases, which took digging through GCP's organization-policy hierarchy (and a propagation delay) to actually resolve.
  • "Looks done" isn't "is done." An AI-generated rebuild of the web dashboard looked pixel-complete but was wired to an entirely invented Firestore schema instead of the real production one — a good reminder to verify the data layer, not just the UI, before trusting a "finished" screen.
  • A UI feature that worked everywhere except where it mattered. Drag-to-reorder for blocks tested fine with a mouse but was unreliable on real Android touch input, so it was removed rather than shipped half-working.

What we learned

Compose Multiplatform is genuinely excellent for keeping business logic and mobile UI in sync across platforms — but Compose-for-Web has a real ceiling once a screen needs to feel like a native web page (SEO, server rendering, browser-native polish). Knowing when to step outside the shared-codebase convenience and reach for a purpose-built web stack, instead of forcing one UI layer everywhere, turned out to matter more than keeping everything "technically multiplatform." On the monetization side, RevenueCat's model extends cleanly to platforms it doesn't officially support (Web) through a one-way webhook mirror into your own database — far simpler and safer than trying to call RevenueCat's API directly from an unsupported client.

What's next

iOS support, premium theme expansion, and closing the remaining gap between the web dashboard's feature set and what mobile already does.

Built With

Share this project:

Updates

Submission history