Runtime configuration schema for Accrue, backed by NimbleOptions.
This module is the single source of truth for supported :accrue
application keys. Host code reads validated values via get!/1 or
Application.get_env/3; extend behaviour through adapters, not by editing
this schema from application code.
Compile-time vs runtime
Adapter atoms (:processor, :mailer, :mailer_adapter,
:invoice_pdf_adapter, :pdf_adapter, :auth_adapter) are stable per-deploy and fine at compile time via
Application.compile_env!/2.
Secrets (:stripe_secret_key) and host-owned fields (:default_currency,
:from_email, brand colors) MUST be read at runtime. See CLAUDE.md
§Config Boundaries.
Options
:repo(atom/0) - Required. HostEcto.Repomodule that Accrue writes to (event ledger, webhook events, billing tables).:processor(atom/0) - Processor adapter implementingAccrue.Processorbehaviour. The default value isAccrue.Processor.Fake.:mailer(atom/0) - Mailer pipeline module implementingAccrue.Mailerbehaviour. The default value isAccrue.Mailer.Default.:mailer_adapter(atom/0) - Swoosh-backed mailer delivery module. The default value isAccrue.Mailer.Swoosh.:invoice_pdf_adapter(atom/0) - Invoice PDF adapter implementingAccrue.InvoiceRendererbehaviour. Defaults to the native Rendro-backed invoice renderer. The default value isAccrue.InvoiceRenderer.Rendro.:pdf_adapter(atom/0) - Legacy HTML-to-PDF adapter implementingAccrue.PDFbehaviour. Used by the Chromic invoice renderer path and by custom HTML callers. The default value isAccrue.PDF.ChromicPDF.:auth_adapter(atom/0) - Auth adapter implementingAccrue.Authbehaviour. The default value isAccrue.Auth.Default.:plan_resolver- Optional host-owned resolver module implementingAccrue.PlanResolver. Required when a processor needs more than a bareprice_idto perform plan changes through the shared facade (for example Braintree swap-plan flows). The default value isnil.:storage_adapter(atom/0) - Storage adapter implementingAccrue.Storagebehaviour. v1.0 shipsAccrue.Storage.Nullonly; hosts supply a custom adapter (e.g., S3) to enable persisted asset storage.Accrue.Storage.Filesystemships in v1.1. The default value isAccrue.Storage.Null.:stripe_secret_key(String.t/0) - Runtime Stripe secret key. MUST be read at runtime only; never viaApplication.compile_env!/2. Validated at boot whenprocessor == Accrue.Processor.Stripe.:stripe_api_version(String.t/0) - Stripe API version pinned by the:lattice_stripewrapper. The default value is"2026-03-25.dahlia".:emails(keyword/0) - Per-email-type switches. Keys are email type atoms; values arebooleanor{Mod, :fun, args}MFA callbacks. The default value is[].:email_overrides(keyword/0) - Per-email-type template module overrides (third rung of the override ladder; seeguides/email.md). Keys are email type atoms; values are module names. The default value is[].:attach_invoice_pdf(boolean/0) - Auto-attach invoice PDF to the receipt email. The default value istrue.:enforce_immutability(boolean/0) - When true,Accrue.Applicationboot raises if the current PG role has UPDATE/DELETE onaccrue_events. The default value isfalse.:business_name(String.t/0) - Business name shown in email headers, PDFs, and admin UI. The default value is"Accrue".:business_address(String.t/0) - Business postal address shown in invoice footers. The default value is"".:logo_url(String.t/0) - Absolute URL to the brand logo used in email + PDF headers. The default value is"".:support_email(String.t/0) - Reply-to support email address for transactional mail. The default value is"support@example.com".:from_email(String.t/0) - Default From: address for transactional mail. The default value is"noreply@example.com".:from_name(String.t/0) - Default From: name for transactional mail. The default value is"Accrue".:default_currency(atom/0) - Default currency when one is not explicitly supplied. The default value is:usd.:webhook_signing_secrets(term/0) - Map of processor atom to signing secret(s). Each value is a string or list of strings for rotation. Example:%{stripe: ["whsec_old", "whsec_new"]}. The default value is%{}.:succeeded_retention_days- Number of days to retain:succeededwebhook events before the Pruner deletes them. Set to:infinityto disable pruning. Default: 14. The default value is14.:dead_retention_days- Number of days to retain:deadwebhook events before the Pruner deletes them. Set to:infinityto disable pruning. Default: 90. The default value is90.:webhook_handlers(list ofatom/0) - List of modules implementingAccrue.Webhook.Handlerbehaviour. Called sequentially after the default handler on each webhook event. Example:[MyApp.BillingHandler, MyApp.AnalyticsHandler]. The default value is[].:portal_mount_path(String.t/0) - Mount path for the first-partyAccrue.Portalroutes when a processor uses local portal semantics. Returned checkout / billing-portal URLs append to this normalized path after:portal_base_urlsupplies the absolute host. The default value is"/billing".:portal_base_url- Absolute base URL (for examplehttps://app.example.com) used to generate returned local portal checkout and billing-portal URLs. Leave unset only when those local portal URLs are not in use;portal_url/1raises instead of falling back to a relative path. The default value isnil.:braintree_client_token_generator(atom/0) - Module used to generate Braintree client tokens for Hosted Fields. Must exportgenerate/1. The default value isBraintree.ClientToken.:expiring_card_thresholds- Strictly-descending list of day thresholds at which the expiring-card reminder email fires ahead of a stored card's expiration. Default:[30, 7, 1]— 30, 7, and 1 days out. The default value is[30, 7, 1].:idempotency_mode- HowAccrue.Actor.current_operation_id!/0behaves when the process dict has no operation_id.:strictraisesAccrue.ConfigError;:warn(the default) generates a random UUID and logs a warning. Set to:strictin production to ensure every outbound processor call carries a deterministic idempotency key. The default value is:warn.:succeeded_refund_retention_days(pos_integer/0) - Number of days to retain:succeededrefund records before pruning Default: 90. The default value is90.:dunning(keyword/0) - Dunning grace-period overlay config.:modeis:stripe_smart_retriesor:disabled;:terminal_actionis:unpaidor:canceled;:grace_daysadds N days past Stripe's last retry before Accrue asks the processor facade to move the subscription to the terminal action.:campaignis the multi-step dunning cadence (see its own doc). The default value is[mode: :stripe_smart_retries, grace_days: 14, terminal_action: :unpaid, telemetry_prefix: [:accrue, :ops], campaign: [enabled: true, steps: [[after_days: 0, key: :reminder, template: Accrue.Emails.InvoicePaymentFailed], [after_days: 5, key: :action_required, template: Accrue.Emails.DunningActionRequired], [after_days: 12, key: :final_notice, template: Accrue.Emails.DunningFinalNotice]]], engine: Accrue.Dunning.Engine.Oban].:mode- The default value is:stripe_smart_retries.:grace_days(pos_integer/0) - The default value is14.:terminal_action- The default value is:unpaid.:telemetry_prefix(list ofatom/0) - The default value is[:accrue, :ops].:campaign- Multi-step dunning cadence (D-04). A keyword list of[enabled: boolean, steps: [...]]where each step is[after_days: non_neg_integer, key: atom, template: atom].after_daysis ABSOLUTE from campaign start, strictly increasing and unique across the list;keyis required and unique (it becomes the Oban-unique step identity). Ships a default journey ON by default (offsets[0, 5, 12]); setcampaign: false(normalized to[enabled: false, steps: []]) to opt out.steps: []whileenabled: trueis a loud error. The last step'safter_daysMUST be<= grace_days(validated loud at boot) so the final notice precedes the sweeper's terminal action. The default value is[enabled: true, steps: [[after_days: 0, key: :reminder, template: Accrue.Emails.InvoicePaymentFailed], [after_days: 5, key: :action_required, template: Accrue.Emails.DunningActionRequired], [after_days: 12, key: :final_notice, template: Accrue.Emails.DunningFinalNotice]]].:engine(atom/0) - Module implementingAccrue.Dunning.Engine. Default:Accrue.Dunning.Engine.Oban(built-in Oban campaign). Set toAccrue.Integrations.Chimewayto delegate orchestration to Chimeway. The default value isAccrue.Dunning.Engine.Oban.
:webhook_endpoints(keyword/0) - Map of endpoint name to[secret:, mode:]for multi-endpoint webhooks. Example:[primary: [secret: "whsec_..."], connect: [secret: "whsec_...", mode: :connect]]. The default value is[].:dlq_replay_batch_size(pos_integer/0) - Number of rows per chunk inAccrue.Webhooks.DLQ.requeue_where/2bulk replay. The default value is100.:dlq_replay_stagger_ms(non_neg_integer/0) - Milliseconds to sleep between chunks during DLQ bulk replay (protects downstream). Default: 1_000. The default value is1000.:dlq_replay_max_rows(pos_integer/0) - Hard cap on bulk replay. Returns{:error, :replay_too_large}unlessforce: trueis passed. Default: 10_000. The default value is10000.:branding(keyword/0) - Branding config. Single source of truth for email + PDF brand.:from_emailand:support_emailare required for any real deploy. See guides/branding.md. The default value is[].:business_name(String.t/0) - The default value is"Accrue".:from_name(String.t/0) - The default value is"Accrue".:from_email(String.t/0) - Required.:support_email(String.t/0) - Required.:reply_to_email- The default value isnil.:logo_url- The default value isnil.:logo_dark_url- The default value isnil.:accent_color- The default value is"#1F6FEB".:secondary_color- The default value is"#6B7280".:font_stack(String.t/0) - The default value is"-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif".:company_address- The default value isnil.:support_url- The default value isnil.:social_links(keyword/0) - The default value is[].:list_unsubscribe_url- The default value isnil.
:default_locale(String.t/0) - Application-wide default locale for email + PDF rendering. Third rung of the locale precedence ladder (after assigns[:locale] and customer.preferred_locale). Bad locales fall back to "en". The default value is"en".:default_timezone(String.t/0) - Application-wide default IANA timezone for datetime rendering. Third rung of the timezone precedence ladder (after assigns[:timezone] and customer.preferred_timezone). Bad zones fall back to "Etc/UTC". The default value is"Etc/UTC".:cldr_backend(atom/0) - Cldr backend module used byAccrue.Workers.Mailer.enrich/2to validate locale strings. Defaults toAccrue.Cldr. The default value isAccrue.Cldr.:connect(keyword/0) - Stripe Connect configuration.:default_stripe_accountis the fallback connected account id used when no per-call override or pdict scope is active (three-level precedence chain).:platform_feeconfigures the default flat-rate fee consumed byAccrue.Connect.platform_fee/2::percentis aDecimalpercentage (e.g.Decimal.new("2.9")for 2.9%),:fixedis anAccrue.Moneyfee in minor units added after the percentage, and:min/:maxoptionally clamp the result. The default value is[default_stripe_account: nil, platform_fee: [percent: Decimal.new("2.9"), fixed: nil, min: nil, max: nil]].:entitlements(keyword/0) - Plan->feature/quota entitlement catalog (host-owned, runtime). Boot-validated and the sameprice_idmay not map to two plans (raisesAccrue.ConfigErrorat boot). See guides/entitlements.md. The default value is[].:plans(keyword/0) - Logical plan name (atom) -> entitlement entry (features/limits/price_ids) The default value is[].:features(list ofatom/0) - The default value is[].:limits(keyword/0) - The default value is[].:price_ids(list ofString.t/0) - The default value is[].
:resolver(atom/0) - Resolver module (Accrue.Entitlements.Resolver behaviour). Default LocalMap. The default value isAccrue.Entitlements.Resolver.LocalMap.:unmapped_action- Behaviour when an active price_id is unmapped. :deny fails closed; never silent-allow. The default value is:deny.:billable- Global billable resolver: a 1-arity fn(conn | socket -> billable | nil). When unset, the default probescurrent_scope.user -> current_user -> nil. Must never raise — fail closed (resolve to nil) on any miss. The default value isnil.:on_deny- Global deny handler. Default is a content-negotiated opaque 403. Accepts:forbidden | {:redirect, path} | {status, body} | fun/2 | {m,f,a}. A per-guardon_deny:opt overrides this; this overrides the built-in 403. A malformed value fails loud at boot (never silently fails open). The default value is:forbidden.:deny_path(String.t/0) - LiveView fallback redirect target for:forbidden/ non-redirectable denies. The default value is"/".:past_due_grace- Entitlement access for :past_due subscriptions. :none (default) fails closed immediately. :dunning honors the dunning grace window (reuses Accrue.Config.dunning()[:grace_days]). A positive integer N honors an entitlement-specific N-day window. Grace grants are affirmative, resolved, configured decisions — never a fail-open. The default value is:none.:stripe_native_sync- Optional Stripe-native entitlement-summary sync. :disabled (default) is fully inert — no webhook reducer runs, no cache table is read or written. :advisory records Stripe entitlement summaries to an advisory cache for audit / telemetry / the admin read-seam; it does NOT change entitled?/has_active_plan? decisions in v1.x (local mapping stays canonical). The enum (not a boolean) reserves room for future additive modes without a breaking config change. The default value is:disabled.
Summary
Functions
Returns the branding config keyword list.
Returns a single branding key. Raises if the key is unknown.
Returns the configured Cldr backend module used by
Accrue.Workers.Mailer.enrich/2 to validate locale strings.
Returns the Connect config keyword list.
Returns the number of days to retain :dead webhook events.
Returns the application default locale string.
Returns the application default IANA timezone string.
Returns the DLQ bulk-replay chunk size.
Returns the hard cap on DLQ bulk-replay rows.
Returns the DLQ bulk-replay inter-chunk stagger in milliseconds.
Returns the dunning grace-period overlay config.
Returns the dunning campaign cadence config (D-07).
Returns whether the multi-step dunning campaign is enabled (D-07).
Returns the configured dunning campaign steps (D-07).
Returns the configured dunning engine module (D-03).
Returns the runtime :entitlements config keyword list (the plan->feature/
quota catalog), or the schema default [] when unset.
Reads a config key from Application.get_env/3, falling back to the
schema default. Raises Accrue.ConfigError if the key is not in the
schema at all (prevents silent typos in downstream code).
Returns the past-due entitlement grace policy: :none (default,
fail-closed), :dunning (reuse the dunning grace window), or a positive
integer of days.
Returns the NimbleOptions schema keyword list. Used by boot-time validation to iterate keys.
Returns the configured Stripe API version string.
Returns the optional Stripe-native entitlement-sync mode: :disabled
(default, fully inert) or :advisory (record summaries to the advisory
cache for audit / telemetry / the admin read-seam — does NOT change
entitled?/has_active_plan? decisions; local mapping stays canonical).
Ergonomic predicate for the Stripe-native sync gate: true when sync is
enabled in any non-:disabled mode, false otherwise. The webhook
reducer dispatch checks this FIRST so the off lane early-returns before
any Repo call.
Returns the number of days to retain :succeeded webhook events.
Validates a keyword list against the Accrue config schema and returns the
normalized form. Raises NimbleOptions.ValidationError on failure.
Reads the current :accrue application env at boot time, filters it to
the schema-known keys, and validates via NimbleOptions.validate!/2.
NimbleOptions :custom validator for :expiring_card_thresholds.
NimbleOptions :custom validator for the :dunning.campaign cadence
(DUN-01, D-04/D-05).
NimbleOptions :custom validator for :branding.accent_color /
:branding.secondary_color. Accepts #rgb, #rrggbb, and
#rrggbbaa hex color strings; rejects anything else.
NimbleOptions :custom validator for the global :entitlements
on_deny handler (Phase 124, ENT-06).
Returns the multi-endpoint webhook config.
Returns the list of user-registered webhook handler modules.
Returns the signing secret(s) for the given processor.
Functions
@spec branding() :: keyword()
Returns the branding config keyword list.
Reads the nested :branding keyword list from app env, merging any
unset keys with their schema defaults so callers can Keyword.fetch!
any valid key without re-running the full NimbleOptions validator on
every call site.
Returns a single branding key. Raises if the key is unknown.
@spec cldr_backend() :: module()
Returns the configured Cldr backend module used by
Accrue.Workers.Mailer.enrich/2 to validate locale strings.
@spec connect() :: keyword()
Returns the Connect config keyword list.
Shape: `[default_stripe_account: String.t() | nil,
platform_fee: [percent: Decimal.t(), fixed: Accrue.Money.t() | nil,
min: Accrue.Money.t() | nil, max: Accrue.Money.t() | nil]]`.
@spec dead_retention_days() :: pos_integer() | :infinity
Returns the number of days to retain :dead webhook events.
@spec default_locale() :: String.t()
Returns the application default locale string.
@spec default_timezone() :: String.t()
Returns the application default IANA timezone string.
@spec dlq_replay_batch_size() :: pos_integer()
Returns the DLQ bulk-replay chunk size.
@spec dlq_replay_max_rows() :: pos_integer()
Returns the hard cap on DLQ bulk-replay rows.
@spec dlq_replay_stagger_ms() :: non_neg_integer()
Returns the DLQ bulk-replay inter-chunk stagger in milliseconds.
@spec dunning() :: keyword()
Returns the dunning grace-period overlay config.
@spec dunning_campaign() :: keyword()
Returns the dunning campaign cadence config (D-07).
Raw read with its own nested default — dunning/0 does not normalize the
nested :campaign key (identical constraint to past_due_grace/0), so
the default journey is supplied here when a host overrides :dunning
without re-stating :campaign. Ships ON by default.
@spec dunning_campaign_enabled?() :: boolean()
Returns whether the multi-step dunning campaign is enabled (D-07).
false when the campaign is opted out (campaign: false /
enabled: false). Fail-closed: an absent :enabled key reads as false.
@spec dunning_campaign_steps() :: [keyword()]
Returns the configured dunning campaign steps (D-07).
Returns [] when the campaign is disabled (so downstream consumers never
fire steps for an opted-out host), else the configured step list.
@spec dunning_engine() :: module()
Returns the configured dunning engine module (D-03).
Defaults to Accrue.Dunning.Engine.Oban (built-in Oban campaign).
Set dunning: [engine: Accrue.Integrations.Chimeway] to opt into
Chimeway orchestration.
The atom is returned as-is without loading the module — resolution happens at dispatch time in the calling code.
@spec entitlements() :: keyword()
Returns the runtime :entitlements config keyword list (the plan->feature/
quota catalog), or the schema default [] when unset.
This is a thin runtime accessor (get!/1, NOT compile_env!) because
:entitlements is host-owned catalog data that legitimately differs per
environment (e.g. test-mode vs live-mode price_ids), like :branding and
:dunning. Unlike branding/0 (which merges schema defaults), this is a
raw runtime read — it does NOT apply the nested per-plan defaults
(features: [], limits: [], price_ids: []). validate_at_boot!/0
validates the config but discards the normalized result and never writes it
back to app env. The resolver tolerates missing nested keys via
Keyword.get/3 defaults, so it reads this raw value without re-running the
full validator.
Reads a config key from Application.get_env/3, falling back to the
schema default. Raises Accrue.ConfigError if the key is not in the
schema at all (prevents silent typos in downstream code).
@spec past_due_grace() :: :none | :dunning | pos_integer()
Returns the past-due entitlement grace policy: :none (default,
fail-closed), :dunning (reuse the dunning grace window), or a positive
integer of days.
The :none default is applied here because entitlements/0 does a raw
read without normalizing nested :entitlements defaults. The
:dunning -> grace_days resolution is done by the resolver when it
widens the fold, not by this accessor.
@spec plan_resolver() :: module() | nil
@spec portal_base_url() :: String.t() | nil
@spec portal_mount_path() :: String.t()
@spec schema() :: keyword()
Returns the NimbleOptions schema keyword list. Used by boot-time validation to iterate keys.
@spec stripe_api_version() :: String.t()
Returns the configured Stripe API version string.
@spec stripe_native_sync() :: :disabled | :advisory
Returns the optional Stripe-native entitlement-sync mode: :disabled
(default, fully inert) or :advisory (record summaries to the advisory
cache for audit / telemetry / the admin read-seam — does NOT change
entitled?/has_active_plan? decisions; local mapping stays canonical).
The :disabled default is supplied here because entitlements/0 does a
raw read without normalizing nested :entitlements defaults (identical
constraint to past_due_grace/0).
@spec stripe_native_sync?() :: boolean()
Ergonomic predicate for the Stripe-native sync gate: true when sync is
enabled in any non-:disabled mode, false otherwise. The webhook
reducer dispatch checks this FIRST so the off lane early-returns before
any Repo call.
@spec succeeded_retention_days() :: pos_integer() | :infinity
Returns the number of days to retain :succeeded webhook events.
Validates a keyword list against the Accrue config schema and returns the
normalized form. Raises NimbleOptions.ValidationError on failure.
@spec validate_at_boot!() :: :ok
Reads the current :accrue application env at boot time, filters it to
the schema-known keys, and validates via NimbleOptions.validate!/2.
Called by Accrue.Application.start/2 before the supervision tree
starts. Raises NimbleOptions.ValidationError on misconfig — fail loud
rather than limp into production with silently-broken config.
Only schema-known keys are validated. Extra keys in the :accrue env
(e.g., per-module adapter configs like Accrue.Mailer.Swoosh) are
ignored here — they belong to their own libraries and would otherwise
produce spurious unknown option errors.
@spec validate_descending(term()) :: {:ok, [pos_integer()]} | {:error, String.t()}
NimbleOptions :custom validator for :expiring_card_thresholds.
Accepts a non-empty list of positive integers that is strictly
descending (each element strictly less than the previous). Returns
{:ok, list} on success, {:error, message} on failure.
NimbleOptions :custom validator for the :dunning.campaign cadence
(DUN-01, D-04/D-05).
Accepts:
false— normalized to[enabled: false, steps: []](opt-out, D-05).- a keyword list
[enabled: boolean, steps: [...]]where each step is a keyword list[after_days: non_neg_integer, key: atom, template: atom]validated against@step_schema.
Intra-list invariants (the cross-field last_step.after_days <= grace_days
invariant is checked separately at boot by
validate_dunning_campaign_grace!/1, since a {:custom} validator cannot
read the sibling :grace_days key):
after_daysstrictly increasing AND unique across the list.keyunique across the list.- when
enabled: true,stepsMUST be non-empty —steps: []while enabled is a LOUD{:error, _}(never a silent disable, D-05).
Returns {:ok, normalized_keyword} on success, {:error, message} on
failure (so boot validation fails loud rather than silently mis-firing).
NimbleOptions :custom validator for :branding.accent_color /
:branding.secondary_color. Accepts #rgb, #rrggbb, and
#rrggbbaa hex color strings; rejects anything else.
NimbleOptions :custom validator for the global :entitlements
on_deny handler (Phase 124, ENT-06).
Accepts any of the supported deny-handler shapes and returns
{:ok, value}; a malformed value returns {:error, message} so boot
validation (validate_at_boot!/0) fails loud rather than letting a
broken deny path silently fail open (T-124-01).
Supported shapes:
:forbidden— built-in opaque 403 / LiveView redirect todeny_path.{:redirect, path}whenpathis a binary.{status, body}whenstatusis an integer andbodyis a binary.- a 2-arity
fun(container, ctx -> result). - an MFA
{m, f, a}whenm/fare atoms andais a list.
@spec webhook_endpoints() :: keyword()
Returns the multi-endpoint webhook config.
@spec webhook_handlers() :: [module()]
Returns the list of user-registered webhook handler modules.
Returns the signing secret(s) for the given processor.
Looks up webhook_signing_secrets in the :accrue application env
and extracts the value for the given processor atom. Returns a list
of strings (for multi-secret rotation support). Raises
Accrue.ConfigError if no secrets are configured for the processor.