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=true claim Auto-login into the per-country seeded demo org Rate-limited: 20/min, 100/hr per IP [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 Rate-limit: demo-session bucket (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_ONLY error code Prevents write pollution in shared demo orgs Stripe Integration Webhook fix (WP1): StripeWebhookRoutes.kt now calls stripeService.syncPlanTier(orgId, subscriptionId) on customer.subscription.created event (previously only audit-logged → org never got stripeSubscriptionId → 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/session returns requiresCardSetup:true when org has no stripeCustomerId Frontend calls POST /billing/checkout (PRO plan) with trial_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=7 env var organizations.trial_started_at/ends_at TrialService.kt (ACTIVE/EXPIRED/PAID states) TrialGatePlugin.kt enforces 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) Previously RS/BA collapsed to .cloud — now fixed Demo Web Route Route: apps/web/app/(auth)/demo/ Calls GET /auth/demo?country= Seeds auth-store as isDemoSession Skips MSAL entirely DemoBanner: Persistent gold banner with mode="conversion" + country prop 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 to app.bilko.cloud ) Copy: "30 dana" → "7 dana" Copy: "bez kreditne kartice" → "kreditna kartica potrebna" Unified demo label: "Pogledaj demo" 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.json off 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/demo endpoint (same as web) DEPLOYMENT Deploy Method Imperative Azure CLI: az containerapp update with 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/demo endpoint live WP4 (Web app) — /demo route 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-demo managed 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 logic routes/StripeWebhookRoutes.kt — syncPlanTier on subscription.created features/TrialGatePlugin.kt — DEMO_READ_ONLY guard auth/BilkoPrincipal.kt — isDemo claim plugins/Authentication.kt — demo JWT parse plugins/RateLimit.kt — demo-session bucket db/migration/Vxx — INTERNAL org_type (V91) features/StripeService.kt / routes/BillingRoutes.kt — reused Web apps/web/lib/api-base.ts — per-country routing lib/msal/use-entra-auth.ts — requiresCardSetup check lib/stores/auth-store.ts — isDemoSession components/DemoBanner.tsx — conversion mode app/(auth)/demo/ — new demo route app/(auth)/checkout/ — new checkout interstitial (WP5, deferred) messages/*.json — i18n copy Mobile apps/mobile/src/auth/entra.ts — API URL + tenant src/api/client.ts — API base eas.json — build config Landings apps/landing-{hr,io,ba}/{components/Hero.tsx,Navbar.tsx,app/page.tsx,+subpages} Infra .github/workflows/azure-deploy.yml pages-deploy-bilko-*.yml DEPLOY-MAP.md apps/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/session mint; POST /billing/checkout returns 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.created doesn't set stripeSubscriptionId → 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-ui dedup extraction (post-launch cleanup) ACA app renaming bilko-*-demo → bilko-*-prod (deferred; labeled via Azure tags bilko-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