Overview
Start Here
Installation
Tutorial
Architect App Builder
Todo List App
Beacon Beats Audio Store
UCAL App
Authoring
Dynamic Apps
Static Apps
dev_docs
Why Pharos Feels Different
Architect Builder
Framework
Artifacts & Layout
Configuration
Outbound Email
How to Use CLI
Contracts
YAML Authoring
Action Hooks and Middleware
Feature Transaction Primitives
Generated Surfaces
Declarative CRM
Input Validation
Identity and Tenancy
UI Surface Library
Social Connections
Payment Provider Management
Integration Adapters
Reference
Contract, UI, and Surface Reference
Styling UI Elements
Architect Media Artifacts
API Docs
Use Cases
Dynamic SPAs
Generated Admin and APIs
Scheduling and Marketplaces
Multi-App Hosting
Release Notes
Vision
Pharos Framework

← Back

In-App Payment Provider Management

Pharos apps can let authorized business or tenant users connect and manage a payment provider from inside the app while the platform operator keeps custody of platform secrets.

The first provider implementation is Stripe Connect. The runtime surface is provider-neutral so future providers can use the same account, secret, audit, callback nonce, route, and UI-state model.

App-owned payment provider contract

An app opts in from data/payment_provider.yml, which is loaded through the app manifest:

owner_resource_type: business
owner_state_section: businesses
owner_route_field: slug
owner_id_field: id
owner_user_id_field: owner_id
tenant_state_section: tenants
tenant_owner_id_field: source_business_id
tenant_id_field: id
tenant_owner_user_id_field: owner_user_id
membership_state_section: tenant_memberships
membership_user_id_field: user_id
membership_tenant_id_field: tenant_id
membership_status_field: membership_status
owner_path_prefix: /businesses/
owner_route_param: business_slug
settings_return_path: /businesses/:business_slug/
policy_gate: manage_business_payments
supported_providers:
  - provider: stripe
    connect_action_id: payment_provider_connect_stripe
    callback_action_id: payment_provider_callback_stripe
    refresh_action_id: payment_provider_refresh_stripe
    disconnect_action_id: payment_provider_disconnect_stripe
    credentials_action_id: payment_provider_credentials_stripe
    credentials_delete_action_id: payment_provider_credentials_delete_stripe
    checkout_mode_action_id: payment_provider_mode_stripe
    credential_fields:
      - name: stripe_client_id
        secret_kind: client_id
        required: true
      - name: stripe_client_secret
        secret_kind: client_secret
        required: true
provider_checkout:
  kind: appointment_booking
  action_id: create_appointment
  provider: stripe
  reference_prefix: APPT
  success_path: /appointments/
  cancel_path: /explore/
  webhook_action_id: payment_provider_checkout_webhook_stripe

The app owns the policy gate, the return path, the owner-to-tenant mapping, the optional owner and tenant membership fields used for authorization, and the templates that present the provider state. The runtime owns callback token verification, provider requests, state transitions, audit events, and secret redaction.

Operator-owned runtime configuration

The operator enables the provider and supplies platform-owned secret references through app.conf or the deployment environment:

payment_providers=stripe
stripe_client_id=<stripe-connect-client-id>
stripe_client_secret_env=PHAROS_STRIPE_CLIENT_SECRET
stripe_webhook_secret_env=PHAROS_STRIPE_WEBHOOK_SECRET
payment_provider_state_key_env=PHAROS_PAYMENT_PROVIDER_STATE_KEY
payment_provider_secret_key_env=PHAROS_PAYMENT_PROVIDER_SECRET_KEY
payment_provider_secret_key_version=v1
# During rotation only: retain one prior key until every encrypted row is rewrapped.
payment_provider_secret_previous_key_version=v0
payment_provider_secret_previous_key_env=PHAROS_PAYMENT_PROVIDER_SECRET_KEY_V0
# Optional local proof overrides. Production should use the default HTTPS Stripe endpoints.
# payment_provider_allow_loopback_http=false
# payment_provider_allow_unsigned_local_webhook=false
# payment_provider_allow_plaintext_local=false
# stripe_oauth_authorize_url=https://connect.stripe.com/oauth/authorize
# stripe_oauth_token_url=https://connect.stripe.com/oauth/token
# stripe_account_api_base_url=https://api.stripe.com/v1/accounts
# stripe_oauth_deauthorize_url=https://connect.stripe.com/oauth/deauthorize
# stripe_checkout_sessions_url=https://api.stripe.com/v1/checkout/sessions
base_url=https://example.test/ucal

stripe_client_id is not a secret. The values referenced by stripe_client_secret_env, payment_provider_state_key_env, and payment_provider_secret_key_env must never be committed to an app or template. payment_provider_secret_key_version identifies the active AES-256-GCM key. During rotation the runtime may decrypt rows tagged with the one declared previous version, then rewrap them with the active key; revocation clears ciphertext before marking the record revoked.

The Stripe endpoint URL settings are optional and exist for local proof harnesses and controlled operator overrides. When omitted, the runtime uses the official HTTPS Stripe endpoints. A non-HTTPS override is rejected unless it points to a loopback HTTP address and payment_provider_allow_loopback_http=true; this is for local smoke tests only and must not be enabled in production config. Unsigned checkout webhooks are accepted only when payment_provider_allow_unsigned_local_webhook=true; that key is also local-only and must not be enabled in production.

base_url must match the externally visible app origin and base path because the runtime derives callback URLs from it.

Business-admin flow

For UCAL, a business owner opens the business settings summary and uses the payment-provider controls rendered from the app templates:

  1. Connect starts Stripe hosted onboarding and records connect_requested and
  2. redirect_pending.

  3. The callback validates the signed state token and single-use nonce, exchanges
  4. the authorization code, refreshes the provider account, and records the resulting durable state.

  5. Refresh re-reads provider account status and can move the account to
  6. connected, restricted, verification_pending, or revoked_externally.

  7. Disconnect records disconnect_requested, calls the provider deauthorization
  8. endpoint, and records disconnected. A duplicate disconnect after that is a no-op.

Errors use stable reason codes and redacted reason detail. Provider response bodies, platform secrets, callback signing keys, and Stripe-style secret tokens must not be stored in audit rows or displayed directly.

State and audit model

The durable account row is keyed by:

  • app_id
  • owner_resource_type
  • owner_resource_id
  • provider_id

This keeps the framework contract reusable beyond UCAL. UCAL maps its business records to owner_resource_type=business; another app can bind the same runtime surface to a different owner resource by changing manifest-declared section and field names rather than runtime code.

The runtime also records:

  • callback nonces, so callbacks are single-use and replay-safe
  • audit events, so connect, callback, refresh, disconnect, and failure states
  • are inspectable

  • provider account snapshots, so UI can render a state summary without treating
  • legacy app-specific booleans as authoritative

Provider-backed checkout

Apps opt in to provider-backed checkout through the app-owned data/payment_provider.yml contract. The runtime dispatch remains generic: the app declares the checkout action, provider, owner mapping, return paths, and webhook action. Stripe is the first concrete provider.

Beacon Beats uses the provider checkout contract for cart checkout:

provider_checkout:
  action_id: complete_purchase
  provider: stripe
  reference_prefix: BB
  owner_route_value: beacon-beats
  success_path: /checkout/success/
  cancel_path: /checkout/cancel/
  webhook_action_id: beacon_payment_provider_webhook_stripe

When owner credentials exist and the owner selects Live mode, Beacon Beats creates a Stripe-hosted Checkout Session from the cart, records the order as payment_pending, and unlocks downloads only after the signed provider webhook confirms payment. Demo remains the default fallback when credentials are absent or the owner selects Demo mode.

UCAL uses the same provider checkout contract for prepaid appointment booking. When a business has saved credentials and selects Live mode, booking a service creates a Stripe-hosted Checkout Session for the service cost and records the appointment as payment_pending. The appointment is confirmed only after the signed provider webhook marks the provider checkout reference as paid.

The checkout webhook handler models these Stripe events:

  • checkout.session.completed: marks the matching live checkout paid.
  • checkout.session.expired: marks the matching checkout expired without
  • granting access.

  • checkout.session.async_payment_failed and payment_intent.payment_failed:
  • mark the matching checkout failed without granting access.

  • charge.refunded, refund.created, and refund.updated: mark the matching
  • checkout refunded; Beacon Beats download entitlements for that order are revoked.

  • charge.dispute.created and charge.dispute.funds_withdrawn: mark the
  • matching checkout disputed; Beacon Beats download entitlements for that order are revoked.

  • payout.paid, payout.failed, and payout.canceled: accepted as
  • reconciliation notifications without checkout-state mutation.