Skip to content

feat(paycraft): track 84 migrations + close anon SECURITY DEFINER surface · native billing · full paywall surface · locale-correct pricing - #157

Draft
therajanmaurya wants to merge 31 commits into
MobileByteLabs:devfrom
therajanmaurya:session-PayCraft-20260902163321251
Draft

feat(paycraft): track 84 migrations + close anon SECURITY DEFINER surface · native billing · full paywall surface · locale-correct pricing#157
therajanmaurya wants to merge 31 commits into
MobileByteLabs:devfrom
therajanmaurya:session-PayCraft-20260902163321251

Conversation

@therajanmaurya

@therajanmaurya therajanmaurya commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Production-hardens the PayCraft SDK and closes the anon-reachable surface on its Postgres. Brings
84 previously-untracked migrations under version control (a blanket supabase/migrations/* ignore
had been silently dropping schema from deploy), adds the native billing lane and the four-state
paywall surface, and lands five security migrations plus a locale-price fix — both driven by
defects found on a real device and in a live database, not by code review.

Changes

Schema now tracked (84 migrations)

  • The .gitignore blanket-ignore + per-file-negation pattern is removed; every migration is
    versioned. This is the defect that left 73 migrations out of CI.
  • New this round: 094 revoke anon on 14 unguarded SECURITY DEFINER writers · 095 tenant guard
    on the 3 still reachable by a logged-in user · 096 lock the crypto primitives + a missed
    upgrade_tenant_plan overload · 097 close the remaining anon surface · 098 drop the OTP gate.

Native billing — Play Billing v9 (PlayBillingNativeClient.android.kt) and a StoreKit 2 bridge,
behind NativeBillingClient / NativeOffer, with OfferSelectionTest + PurchaseLoopTest.

Paywall surface — every BillingState arm renders: ConfigResilienceUi,
EmptyProductsContent, DeviceConflictContent, OwnershipVerifiedContent, BuiltInPaywall,
PayCraftSurfaceMode, plus a bundled paycraft-fallback.json for the offline/cold-start path.

Locale-correct pricing — all 8 render sites now resolve through the precedence rule
(native store price > cloud per-locale price > base price) instead of formatting the base price.

OTP gate removed — SDK actions/handlers/UI, 4 test tags, 4 strings, the otp-send-hook Edge
Function, and the DB objects. Device conflicts resolve via OAuth or support.

Dependency holds — Supabase held at 3.1.4 and Store5 at 5.1.0-alpha10; Kermit removed in
favour of an internal PlatformLog.

AI SoT corpuspaycraft-instructions/ (BILLING_STATE_SEMANTICS, FAILURE_MODES,
KEY_TIERING, PAYWALL_CUSTOMIZATION, MANIFEST.yaml).

Three defects carried out of the epic, now closed

  • 099 restores ON CONFLICT (tenant_id, sku) to tenant_products_upsert. This was a REGRESSION:
    058 had already fixed it, and 087 re-declared the function "copied VERBATIM from 084" — carrying
    084's ON CONFLICT (id) back in and silently undoing it. A dashboard PATCH sending a fresh UUID
    for an existing SKU therefore raised on the (tenant_id, sku) unique constraint instead of
    updating. id is deliberately omitted from the UPDATE arm so a SKU match keeps its primary key
    and existing references stay valid.
  • app/api/products/[id]/route.ts now selects the full row and merges it beneath the patch. PATCH
    is partial by definition but the RPC takes a full row, so a payload changing only active — the
    enable/disable toggle — arrived with no sku and hit a NOT NULL violation surfaced as a 500. The
    same narrow select was the audit log's before-image, so product.updated entries could not show
    what changed; they now carry the whole row.
  • supabase/functions/config/index.ts fails loudly on paywallRes, providersRes and tenantRes
    instead of degrading them to defaults. providersRes mattered most: an outage became zero
    providers, rendering a paywall with no way to pay that looks like tenant misconfiguration.
    tenantRes separates a missing row (PGRST116 → 404) from a transport failure (→ 500).

Notable files

supabase/migrations/096 — an unguarded decryption oracle. decrypt_provider_key(bytea) loads
the server passphrase and returns pgp_sym_decrypt(...) with no authorisation, and anon held
EXECUTE. It is the primitive that tenant_providers_decrypt_key and tenant_stripe_connect_decrypt
are built on — both correctly guarded — so the guards could be walked around by calling the layer
beneath them. Static review could not see it: the scan classified functions by whether they write
rows, and this one only reads. It surfaced only when the live catalog was asked what anon can
execute (14 by file inspection; 61 by probe).

supabase/migrations/097 — a guard that was inert against the caller it had to stop.
tenant_pricing_bulk_upsert used IF auth.uid() IS NOT NULL AND NOT EXISTS(tenant_admins…). That
prefix exists so service_role (NULL uid) passes — but anon also has a NULL uid, so the
condition short-circuited and the write proceeded. Reads as guarded, was wide open.

gradle/libs.versions.toml — the holds are load-bearing. Publishing at the previous dependency
set crashed every consumer at launch: PayCraft had moved above consumer pins, inverting Gradle's
resolution. Store5 alpha11 was the sole requester of Kermit 2.1.0 in a consumer's graph — every
other requester asked for 2.0.x — so removing PayCraft's direct Kermit dependency did not fix it;
holding Store5 did.

ui/ProductList.kt:396 — JVM-green, Android-fatal. Regex("\{[A-Za-z_]+}") compiles on the JVM
and throws PatternSyntaxException on Android's ICU engine. As a top-level val the throw lands in
<clinit>, so every paywall render died with ExceptionInInitializerError. 385 unit tests and 14
Roborazzi goldens passed throughout — they all run on the JVM.

Verification

382 tests pass · 14/14 Roborazzi goldens unchanged · deno check clean on the config function ·
tsc --noEmit exit 0 on the dashboard · G-PAYCRAFT-DEPLOY passes (it caught 099 as untracked before
this was called done) · migrations 094–098 applied and privilege-probed
against a local Postgres (crypto primitives closed, both upgrade_tenant_plan overloads closed,
9/9 SDK RPCs still anon-executable) · paywall device-verified on CPH2423 / Android 15 rendering a
single currency end-to-end.

Not verified: migrations 094–099 are unapplied in PRODUCTION — every privilege claim above was
measured against a local Postgres. Migration 099's conflict-target change has not been exercised
against real data, and no iOS storefront capture exists yet (AC-13).

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…om/mobilebytelabs/paycraft/presentation/ProviderBottomSheet.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/presentation/templates/BrandedStackTemplate.kt
…ytelabs/paycraft/billing/PlayBillingNativeClient.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/billing/NativeBillingClient.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt
…telabs/paycraft/core/PayCraftBillingManager.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/network/PayCraftService.kt cmp-paycraft/src/iosMain/kotlin/com/mobilebytelabs/paycraft/billing/PlatformNativeBillingClient.ios.kt
…telabs/paycraft/PayCraft.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/di/PayCraftModule.kt
…ytelabs/paycraft/billing/PlayBillingNativeClient.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/billing/NativeBillingClient.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt
…ytelabs/paycraft/billing/PlayBillingNativeClient.android.kt gradle/libs.versions.toml
…androidMain/kotlin/com/mobilebytelabs/paycraft/billing/PlayBillingNativeClient.android.kt gradle/libs.versions.toml
…ytelabs/paycraft/PayCraftPlatform.android.kt cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/billing/PlayBillingNativeClient.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/debug/PayCraftLogger.kt
…ample-app/src/androidTest/kotlin/com/mobilebytelabs/paycraft/sample/BasePayCraftUiTest.kt
…ample-app/src/androidInstrumentedTest/kotlin/com/mobilebytelabs/paycraft/sample/BasePayCraftUiTest.kt
…AUNCH_RUNBOOK.md plugin-template/build.gradle.kts
@therajanmaurya therajanmaurya changed the title Improve and make ready for product and launch feat(billing): production-harden PayCraft — Play Billing v9, StoreKit 2 bridge, paywall surface fix, AI SoT corpus Sep 4, 2026
…/[id]/pricing/page.tsx dashboard/app/api/products/[id]/route.ts
…packages.sql supabase/migrations/090_lifetime_entitlement_semantics.sql
…m/mobilebytelabs/paycraft/billing/PayCraftStoreKit.kt dashboard/lib/customer-geo.ts
…/com/mobilebytelabs/paycraft/PayCraft.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/config/ConfigClient.kt
…eResources/values/strings.xml cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt
…T.yaml idea-layer/state/PAYCRAFT_PHASE4_VERDICTS.yaml
…-shadow.ts supabase/functions/config/index.ts
…eResources/values/strings.xml cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/config/ConfigResult.kt
…androidMain/kotlin/com/mobilebytelabs/paycraft/PayCraftPlatform.android.kt cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/billing/PlayBillingNativeClient.android.kt
@therajanmaurya therajanmaurya changed the title feat(billing): production-harden PayCraft — Play Billing v9, StoreKit 2 bridge, paywall surface fix, AI SoT corpus feat(paycraft): track 84 migrations + close anon SECURITY DEFINER surface · native billing · full paywall surface · locale-correct pricing Sep 6, 2026
…se/functions/config/index.ts supabase/migrations/099_restore_upsert_natural_key.sql
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants