# 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_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:

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

<table id="bkmrk-wpmc-%23titleownerstat"><thead><tr><th>WP</th><th>MC #</th><th>Title</th><th>Owner</th><th>Status</th></tr></thead><tbody><tr><td>WP1</td><td>\#103798</td><td>Stripe enablement + webhook fix</td><td>FlowForge + CodeCraft</td><td>\[verify\]</td></tr><tr><td>WP2</td><td>\#103799</td><td>Instant demo endpoint</td><td>CodeCraft</td><td>\[verify\]</td></tr><tr><td>WP3</td><td>\#103800</td><td>Landing CTA + copy fixes</td><td>Vizu</td><td>\[verify\]</td></tr><tr><td>WP4</td><td>\#103801</td><td>Demo web frontend</td><td>Vizu + CodeCraft</td><td>\[verify\]</td></tr><tr><td>WP5</td><td>\#103802</td><td>Card-required trial flow</td><td>Vizu + CodeCraft</td><td>DEFERRED (Stripe keys blocker)</td></tr><tr><td>WP6</td><td>\#103803</td><td>Mobile wiring</td><td>Skybound</td><td>\[verify\]</td></tr><tr><td>WP7</td><td>\#103804</td><td>Infra hygiene</td><td>FlowForge</td><td>\[verify\]</td></tr><tr><td>WP8</td><td>\#103805</td><td>Validation + docs</td><td>Proveo/Angie + Skillforge</td><td>In progress (this page)</td></tr><tr><td>Deploy</td><td>\#103833</td><td>Azure imperative deploy cutover</td><td>FlowForge</td><td>\[verify\]</td></tr></tbody></table>

## 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&lt;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*