Bilko Customer Funnel Architecture
Bilko Customer Funnel Architecture
Context: This page documents the customer acquisition funnel for Bilko (Azure production), covering instant demo and card-required trial flows across three country markets: HR (bilko.cloud), RS (bilko.io), and BA (bilko.company). Built as MC parent #103797, WP8 docs deliverable #103805.
THE FUNNEL
Each country landing (bilko.{cloud,io,company}) presents two CTAs:
- [Pogledaj demo] — Instant shared demo
- Public GET
/api/v1/auth/demo?country=HR|RS|BA - No signup required
- Returns 60-minute read-only demo JWT with
demo=trueclaim - Auto-login into the per-country seeded demo org
- Rate-limited: 20/min, 100/hr per IP
- Public GET
- [Probaj 7 dana] — Card-required 7-day trial
- Entra CIAM signup (bilkociam tenant 20bb17de-9be5-4143-a7e5-8c1ddae6a064)
- Card-required Stripe checkout (WP5, currently DEFERRED until live Stripe keys provisioned)
- 7-day trial period
- Auto-charge on day 8
Funnel Flow Diagram
Landing bilko.{cloud,io,company}
├─ [Pogledaj demo] → app.bilko.{cloud,io,company}/demo?country=HR|RS|BA
│ → GET /api/v1/auth/demo?country= (public, no signup)
│ → 60-min read-only demo JWT → /dashboard + DemoBanner(conversion mode)
└─ [Probaj 7 dana] → app.bilko.{cloud,io,company}/register?country=…&plan=trial
→ Entra CIAM signup → JIT org (BASIC tier, trialEndsAt=+7d)
→ [DEFERRED] FORCED Stripe Checkout (card, trial_period_days=7, payment_method_collection=ALWAYS)
→ subscription.created webhook → syncPlanTier → /dashboard (trial active)
→ day 8: invoice.payment_succeeded → ACTIVE/PRO
BACKEND ARCHITECTURE
Demo Endpoint
- Route:
GET /api/v1/auth/demo?country=HR|RS|BA(public, in authPublicRoutes) - Identity: Backend-issued short-lived (60 min) Bilko-JWT (not MSAL)
- Claims:
demo:true, mapped to one shared demo org per country - Demo orgs (V91 migration):
- RS:
00000000-0000-0014-a000-000000000001(RSD currency) - HR:
00000000-0000-0029-c000-000000000001(EUR currency) - BA:
[verify](BAM currency) - All marked
org_type=INTERNAL
- RS:
- Rate-limit:
demo-sessionbucket (20/min, 100/hr per IP) + CF edge rate-limit on/auth/demo
DEMO_READ_ONLY Guard
- TrialGatePlugin.kt: Blocks non-GET requests when
BilkoPrincipal.isDemo == true - Returns HTTP 403
DEMO_READ_ONLYerror code - Prevents write pollution in shared demo orgs
Stripe Integration
- Webhook fix (WP1):
StripeWebhookRoutes.ktnow callsstripeService.syncPlanTier(orgId, subscriptionId)oncustomer.subscription.createdevent (previously only audit-logged → org never gotstripeSubscriptionId→ user locked out after 7d despite card) - Webhook URL:
https://api.bilko.cloud/api/v1/webhooks/stripe - Events:
customer.subscription.created/updated/deleted,invoice.payment_succeeded/failed - Card-required trial (WP5, DEFERRED):
/auth/entra/sessionreturnsrequiresCardSetup:truewhen org has nostripeCustomerId- Frontend calls
POST /billing/checkout(PRO plan) withtrial_period_days=7+payment_method_collection=ALWAYS - Redirect to Stripe before
/dashboard - Blocker: Requires live Stripe keys (sk_live, whsec) + prices in Azure KV kv-bilko-demo
Trial Engine (Reused)
TRIAL_DAYS=7env varorganizations.trial_started_at/ends_atTrialService.kt(ACTIVE/EXPIRED/PAID states)TrialGatePlugin.ktenforces trial gate- Plan tiers in
FeatureAccess.kt
FRONTEND ARCHITECTURE
Per-Country API Routing
- File:
apps/web/lib/api-base.ts - Routing:
- RS →
api.bilko.io - BA →
api.bilko.company - HR →
api.bilko.cloud(default)
- RS →
- Previously RS/BA collapsed to
.cloud— now fixed
Demo Web Route
- Route:
apps/web/app/(auth)/demo/ - Calls
GET /auth/demo?country= - Seeds
auth-storeasisDemoSession - Skips MSAL entirely
- DemoBanner: Persistent gold banner with
mode="conversion"+countryprop - Shows "Pokreni 7-dnevni trial" CTA → per-country
/register?plan=trial - 60-min expiry enforced (JWT exp)
Landing CTA Fixes (WP3)
- Files: All 3 landings
components/Hero.tsx,Navbar.tsx,app/page.tsx+ subpages (sef-rezerva,fisk,prebaci-*) - Changes:
- Killed dead
/rs|ba/*links - Per-country app domains: correct
app.bilko.{tld}(was hardcoded toapp.bilko.cloud) - Copy: "30 dana" → "7 dana"
- Copy: "bez kreditne kartice" → "kreditna kartica potrebna"
- Unified demo label: "Pogledaj demo"
- Killed dead
- i18n:
messages/{sr-Latn,hr,bs,en,sr-Cyrl}.json
Checkout Interstitial (WP5, DEFERRED)
- Route:
/checkout - Stripe Embedded Checkout (Bilko-styled)
- GDPR disclosure: "kartica se naplaćuje [date+7]" before redirect
- Blocker: Pending live Stripe keys
MOBILE ARCHITECTURE
- API URL fix:
DEFAULT_API_URL(entra.ts, client.ts) +eas.jsonoff dead GCP stage → Azure prod API - Entra tenant:
EXPO_PUBLIC_ENTRA_ISSUER/CLIENT_ID→bilkociam(20bb17de-9be5-4143-a7e5-8c1ddae6a064) — same as web (was previously 3454a03f → would create duplicate orgs) - Country selector: First-launch country selector (AsyncStorage) →
?country=param on session - Mobile demo: Via
/auth/demoendpoint (same as web)
DEPLOYMENT
Deploy Method
- Imperative Azure CLI:
az containerapp updatewith ACR server-side build - Reason: Azure DevOps CI/CD pipeline (Bilko-CI-CD) is not yet green (tracked MC #103853)
- RLS isolation E2E: Hard data-breach gate (instant-demo-rls-isolation.spec.ts)
Deploy Order (RISK-09)
Landing (CF Pages) vs app (ACA) are independent pipelines. Deploy order MUST be:
- WP2 (API) —
/auth/demoendpoint live - WP4 (Web app) —
/demoroute - WP3 (Landing pages) — CTAs point to live endpoints
Open Go-Live Prerequisites
- WP5/Stripe live keys + prices in Azure KV kv-bilko-demo
- Mobile OAuth client-id registration
mi-bilko-demomanaged identity attached to bilko-api-demo ACA + granted Key Vault Secrets User role
WORKSTREAMS
| WP | MC # | Title | Owner | Status |
|---|---|---|---|---|
| WP1 | #103798 | Stripe enablement + webhook fix | FlowForge + CodeCraft | [verify] |
| WP2 | #103799 | Instant demo endpoint | CodeCraft | [verify] |
| WP3 | #103800 | Landing CTA + copy fixes | Vizu | [verify] |
| WP4 | #103801 | Demo web frontend | Vizu + CodeCraft | [verify] |
| WP5 | #103802 | Card-required trial flow | Vizu + CodeCraft | DEFERRED (Stripe keys blocker) |
| WP6 | #103803 | Mobile wiring | Skybound | [verify] |
| WP7 | #103804 | Infra hygiene | FlowForge | [verify] |
| WP8 | #103805 | Validation + docs | Proveo/Angie + Skillforge | In progress (this page) |
| Deploy | #103833 | Azure imperative deploy cutover | FlowForge | [verify] |
CRITICAL FILES
Backend
apps/api/.../routes/AuthRoutes.kt— /auth/demo, requiresCardSetup logicroutes/StripeWebhookRoutes.kt— syncPlanTier on subscription.createdfeatures/TrialGatePlugin.kt— DEMO_READ_ONLY guardauth/BilkoPrincipal.kt— isDemo claimplugins/Authentication.kt— demo JWT parseplugins/RateLimit.kt— demo-session bucketdb/migration/Vxx— INTERNAL org_type (V91)features/StripeService.kt/routes/BillingRoutes.kt— reused
Web
apps/web/lib/api-base.ts— per-country routinglib/msal/use-entra-auth.ts— requiresCardSetup checklib/stores/auth-store.ts— isDemoSessioncomponents/DemoBanner.tsx— conversion modeapp/(auth)/demo/— new demo routeapp/(auth)/checkout/— new checkout interstitial (WP5, deferred)messages/*.json— i18n copy
Mobile
apps/mobile/src/auth/entra.ts— API URL + tenantsrc/api/client.ts— API baseeas.json— build config
Landings
apps/landing-{hr,io,ba}/{components/Hero.tsx,Navbar.tsx,app/page.tsx,+subpages}
Infra
.github/workflows/azure-deploy.ymlpages-deploy-bilko-*.ymlDEPLOY-MAP.mdapps/edge-proxy/— CF Worker source (newly committed)
VALIDATION GATES (Proveo)
E2E Matrix
HR/RS/BA × {instant demo, card-trial} × {web, mobile-API}
Critical Tests
- instant-demo-rls-isolation.spec.ts: HARD SECURITY GATE — create sentinel contact in real org, then with demo JWT assert NOT visible (200 = data-breach → DO NOT DEPLOY)
- Instant demo API contract:
/auth/demo?country=→ 200, right demo-org UUID + currency; no?country=→ 400; demo JWT exp ≤ 2h; non-GET → 403 DEMO_READ_ONLY - Instant demo web: Landing [Pogledaj demo] → no signup, correct per-country app domain, DemoBanner visible, correct currency + VAT rates (HR 25/13/5, RS 20/10, BA 17), write blocked, 60-min expiry
- Card trial (headless): Pre-seeded CIAM test users +
/auth/test/sessionmint;POST /billing/checkoutreturns real Stripe URL; Stripe CLI trigger scenarios (trialing→PAID, day-8 charge, payment_failed→EXPIRED gate 403) - Link-integrity regression: Every CTA HEAD<400, no dead
/rs|ba|hr/{signup,demo}, demo CTA present, copy contains "7 dana" (NOT "30 dana"), no wrong-country app domain - Mobile API smoke: Demo token per country (currency), demo write→403, trial start ACTIVE, expired→403
KEY RISKS
- RISK-01: Stripe keys missing → StripeService throws on startup (BLOCKER for WP1)
- RISK-02:
subscription.createddoesn't setstripeSubscriptionId→ card-enrolled users locked out at day 7 (FIXED in WP1) - RISK-03: Demo write pollution / shared-JWT (MITIGATED: isDemo block + nightly reset + rate-limit)
- RISK-05: Mobile Entra tenant mismatch → duplicate orgs (FIXED in WP6: unified to bilkociam 20bb17de)
- RISK-09: Landing vs app deploy order (DOCUMENTED: deploy API → web → landing)
- mi-bilko-demo unattached: KV secretref silent fail (STOPGAP: direct env inject)
OUT OF SCOPE
- Resource-group / Postgres-server rename (separate windowed task)
- Native mobile IAP for trial (web checkout in browser for now)
packages/landing-uidedup extraction (post-launch cleanup)- ACA app renaming
bilko-*-demo→bilko-*-prod(deferred; labeled via Azure tagsbilko-role=production)
Source of truth: Plan ~/.claude/plans/fluttering-swimming-cherny.md + MC parent #103797.
This page documents WP8 deliverable #103805 (Skillforge docs).
Last updated: 2026-06-17
No comments to display
No comments to display