This guide mirrors the checked-in examples/accrue_host story in package-facing
terms. Your Phoenix app owns MyApp.Billing, routing, auth, runtime config,
and verification choices. Accrue owns the billing engine behind those public
boundaries. Read-only processor queries such as saved payment methods use
Accrue.Billing.list_payment_methods/2 (and the host wrapper MyApp.Billing.list_payment_methods/2 after mix accrue.install); see guides/telemetry.md for the [:accrue, :billing, :payment_method, :list] span.
Server-side checkout session creation uses Accrue.Billing.create_checkout_session/2
(and your host facade after install); telemetry is [:accrue, :billing, :checkout_session, :create]
— see guides/telemetry.md#billing-checkout-session-create.
Customer Portal session creation is the parallel server-side helper for billing self-service.
Server-side billing portal session creation uses Accrue.Billing.create_billing_portal_session/2
(and your host facade after install); telemetry is [:accrue, :billing, :billing_portal, :create]
— see guides/telemetry.md#billing-billing-portal-create.
Provider behavior stays honest across the shared facade:
| Provider | Checkout session result | Billing portal result |
|---|---|---|
| Stripe | Upstream hosted URL | Upstream hosted URL |
| Braintree | Mounted local URL | Mounted local URL |
This guide mirrors only the setup-critical needles from the canonical processor support matrix: the shared checkout and portal facade stays provider-honest, update_customer/2 remains a bounded provider-neutral write-through, cancel/2 is the shared immediate path, and the official active-subscription-change contract is swap_plan/3 plus preview_upcoming_invoice/2. Preview is the canonical path where supported before commit; swap_plan/3 is native on Stripe, testing/local-only on Fake, bounded on Braintree when the host configures :plan_resolver, and cancel_at_period_end/2 stays a Fake/Stripe-only scheduled-end path.
How to enter this guide
This guide is one spine with three entry capsules — pick where you are starting, then follow the same ordered story (deps → install → runtime → migrations → Oban → webhooks → admin → proof). Public wording and step order stay aligned with examples/accrue_host/README.md; when the spine or command vocabulary changes, update that README in the same pull request (D-02). Maintainer checklist (INT-11): same-PR capsule discipline lives in the contributor map scripts/ci/README.md — search for First Hour + host README capsule parity.
For maintenance posture (when to stop speculative doc work, how friction is intake-gated), see Maturity and maintenance.
Capsule H — Hex consumer
You already have a Phoenix app. Add Accrue to mix.exs, run mix deps.get, then mix accrue.install … and continue from § 1. First run below (runtime config → migrations → Oban → webhook route → admin mount → subscription + proof).
Capsule M — Monorepo clone
From the repository root: cd examples/accrue_host, run mix setup, start mix phx.server, then follow the numbered host README story (subscription → signed webhook → admin → mix verify) — the same Fake-backed arc this guide describes in package terms. Sigra is wired in the checked-in demo for convenience and reproducibility, not because production Accrue apps must use it.
Capsule R — Evaluate / read-only
Shortest read-only path: clone the repo, cd examples/accrue_host, run mix verify or mix verify.full. For merge-blocking VERIFY-01 detail and Playwright entry points, use #proof-and-verification in the host README when you need more than the bounded proof commands.
Trust boundary (production vs demo)
Production apps integrate billing through host-owned Accrue.Auth; see Auth adapters for adapter choices and wiring contracts. Sigra is optional: the demo uses it for deterministic organization billing and CI, not as a blanket production requirement. When you are not on Sigra, follow Capsule H and Organization billing (non-Sigra) for org-scoped Stripe customers. Demo-specific mix.exs and setup commands stay in examples/accrue_host/README.md.
When you are preparing a real deploy (not the demo loop), walk Production readiness once — it links the same guides in ship order without duplicating them.
Stripe-first spine, early Braintree branch
Stay on the default Stripe-hosted path unless you already know you need Braintree. Stripe remains the fastest first-user production route through this guide, while the bounded Braintree branch keeps the same public facade with mounted local checkout and portal behavior.
If you are using Braintree instead, branch early and treat the mounted portal contract as part of initial setup, not a later polish task:
- add
accrue_portal - mount
accrue_portal "/billing"as a sibling scope besideaccrue_admin - set
portal_mount_pathto the mounted portal route - set
portal_base_urlto an absolute host URL - configure
:plan_resolverif you want first-party plan swaps throughAccrue.Billing.swap_plan/3oraccrue_admin - use
Accrue.Billing.preview_upcoming_invoice/2as the canonical preview-before-commit path where supported; Braintree remains unsupported - keep auth/session continuity across the Plug and LiveView portal mounts
- satisfy the Hosted Fields script and CSP contract before opening checkout
That branch stays intentionally short here. Use
Braintree local portal for the full mounted-path
setup, the packaged accrue_portal default story, and the sharp failure modes
to expect when portal_mount_path, portal_base_url, auth/session state, or
Hosted Fields readiness are wrong.
1. First run
The first hour should end with one Fake-backed subscription, one signed webhook proof, mounted admin inspection, and a focused verification pass.
Install the packages
Hex vs
main: The version pins below mirroraccrue/mix.exsandaccrue_admin/mix.exs@versionon the branch you are reading (usuallymainon GitHub). Hex.pm / Hex.pm/packages/accrue_admin reflect what is published; use HexDocs when you need docs tied to the resolved Hex version.
- The fenced
~>pins below track the Hex-published SemVer line for the@versionpair this branch ships with. path:/ monorepo installs must keepaccrueandaccrue_adminon the same three-part~>(lockstep trains).- Pre-1.0
~>minors may still ship breaking API changes—treatmix.lockas the production stability boundary, not semver intuition alone.
defp deps do
[
{:accrue, "~> 1.1.0"},
{:accrue_admin, "~> 1.1.0"}
]
endmix deps.get
mix accrue.install --billable MyApp.Accounts.User --billing-context MyApp.Billing
The checked-in host example is the canonical local evaluation loop:
cd examples/accrue_host
mix setup
mix phx.server
Keep runtime config host-owned
Accrue raises Accrue.ConfigError when required setup is missing. Keep secrets
and environment-specific values in config/runtime.exs:
import Config
config :accrue, :processor, Accrue.Processor.Fake
config :accrue, repo: MyApp.Repo
config :accrue, :webhook_signing_secrets, %{
stripe: System.get_env("STRIPE_WEBHOOK_SECRET", "whsec_test_host")
}Run your database setup before boot:
mix ecto.create
mix ecto.migrate
Invoice rendering now defaults to Rendro, so the normal invoice path does not require Chrome. Only hosts that explicitly choose the legacy Chromic compatibility path need ChromicPDF setup; the deeper renderer and migration details live in PDF Rendering.
Start Oban with the app so webhook dispatch and replay work end to end:
children = [
MyApp.Repo,
{Oban, Application.fetch_env!(:my_app, Oban)},
MyAppWeb.Endpoint
]Mount the public billing boundaries
Add signed webhook ingest at /webhooks/stripe and keep the handler on the
public callback surface:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
import Accrue.Router
pipeline :accrue_webhook_raw_body do
plug Plug.Parsers,
parsers: [:json],
pass: ["*/*"],
json_decoder: Jason,
body_reader: {Accrue.Webhook.CachingBodyReader, :read_body, []}
end
scope "/webhooks" do
pipe_through :accrue_webhook_raw_body
accrue_webhook "/stripe", :stripe
end
endWhen this fails
- Raw body / parser order: Troubleshooting —
ACCRUE-DX-WEBHOOK-RAW-BODY- Missing signing secret: Troubleshooting —
ACCRUE-DX-WEBHOOK-SECRET-MISSING- Webhook behind the wrong pipeline: Troubleshooting —
ACCRUE-DX-WEBHOOK-PIPELINEmix accrue.installreruns / conflicts: Upgrade — installer rerun behavior
defmodule MyApp.BillingHandler do
use Accrue.Webhook.Handler
@impl Accrue.Webhook.Handler
def handle_event(type, event, ctx) do
MyApp.Billing.handle_webhook(type, event, ctx)
end
endMount accrue_admin "/billing" behind your host auth boundary.
AccrueAdmin.Router.accrue_admin/2 is the public router macro:
import AccrueAdmin.Router
scope "/" do
pipe_through [:browser, :require_authenticated_user]
accrue_admin "/billing",
session_keys: [:user_token],
on_mount: [{MyAppWeb.UserAuth, :mount_current_user}]
endProve the first subscription and webhook
Create the first subscription through the generated facade:
user = MyApp.Accounts.get_user!(user_id)
{:ok, subscription} =
MyApp.Billing.subscribe(user, "price_basic", trial_end: {:days, 14})For app-level tests, stay on supported helpers:
use Accrue.TestThen post one signed customer.subscription.created payload through
/webhooks/stripe, visit /billing, and confirm the mounted admin UI shows the
resulting billing state plus replay visibility.
Finish the guided path with the focused host proofs:
mix verify
mix verify is the focused tutorial proof suite. mix verify.full is the
CI-equivalent local gate that adds compile, assets, dev boot, regression, and
browser smoke after the first-run story is already clear. For the authoritative
merge-blocking command matrix, VERIFY-01, and Playwright entry points, see
Proof and verification in the host demo README.
2. Seeded history
Seeded history is for deterministic replay/history evaluation, not for the
main teaching path.
cd examples/accrue_host
mix setup
mix verify.full
Use it when you need replay-ready webhook states, browser smoke fixtures, or other evaluation setup that should not become public integration guidance.
3. Focused verification
mix verifyproves the host-owned tutorial arc: installer boundary, first subscription throughMyApp.Billing, signed webhook ingest, mounted/billinginspection, and replay visibility.mix verify.fullis the CI-equivalent local gate for maintainers.bash scripts/ci/accrue_host_uat.shis the repo-root wrapper around that same full contract.bash scripts/ci/accrue_host_hex_smoke.shis Hex smoke and stays separate from the checked-in host demo.mix accrue.installremains the production setup command for your own host app.
4. Rerunning mix accrue.install
Reruns refresh pristine generated files that still match the Accrue
fingerprint marker; user-edited generated files are skipped so local policy
changes are preserved. Unmarked existing files stay skipped unless you opt into
a narrow overwrite, and --write-conflicts writes reviewable artifacts under
.accrue/conflicts/ instead of patching live files blindly — the same contract
as the upgrade guide. See
Upgrade guide — Installer rerun behavior
for the full installer rerun semantics.