Skip to main content

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:

  1. [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
  2. [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_IDbilkociam (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:

  1. WP2 (API) — /auth/demo endpoint live
  2. WP4 (Web app) — /demo route
  3. 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

WPMC #TitleOwnerStatus
WP1#103798Stripe enablement + webhook fixFlowForge + CodeCraft[verify]
WP2#103799Instant demo endpointCodeCraft[verify]
WP3#103800Landing CTA + copy fixesVizu[verify]
WP4#103801Demo web frontendVizu + CodeCraft[verify]
WP5#103802Card-required trial flowVizu + CodeCraftDEFERRED (Stripe keys blocker)
WP6#103803Mobile wiringSkybound[verify]
WP7#103804Infra hygieneFlowForge[verify]
WP8#103805Validation + docsProveo/Angie + SkillforgeIn progress (this page)
Deploy#103833Azure imperative deploy cutoverFlowForge[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-*-demobilko-*-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