Environment variables
Stripe: optional, and an adapter
Fresh accounts run without Stripe. Without it, the invoice lifecycle is fully functional — invoices generate at subscribe and cycle close, and you settle them viaPOST /api/v1/client/billing/invoices/{id}/mark-paid.
Connecting a Stripe key (via the dashboard or the MCP Stripe setup tools)
switches the account to Stripe-driven billing: checkout sessions, webhook
sync, and meter forwarding. Billing state stays in Tanso either way — Stripe
is the payment adapter, not the source of truth.
A few things happen on their own once Stripe is connected:
- Webhook events stay current. On startup Tanso adds any Stripe events it
handles to its webhook destination in each connected Stripe account. It
only adds events, never removes any. Turn it off with
app.stripe-event-destination-sync-enabled=false, for example on a dev database whose accounts hold real Stripe keys. - Return pages. After a Stripe Checkout page, the human lands on
/public/checkout/completeor/public/checkout/cancelledon your instance, unless the account setsstripeCheckoutSuccessUrl/stripeCheckoutCancelUrl. - Upgrades charge immediately. On Stripe-billed accounts an upgrade between in-advance plans charges the prorated amount during the call, and the plan moves once the payment succeeds. Downgrades take effect at the end of the period.
Behind a proxy
With theprod profile (forward-headers-strategy: framework), the per-IP
agent signup cap reads the leftmost X-Forwarded-For entry. Your
proxy or load balancer must overwrite that header, not append to it,
otherwise a caller can pick its own IP and the per-IP cap stops working. The
per-account cap holds either way.
Database & migrations
Liquibase applies the full schema on startup — an empty PostgreSQL is all you need. Schema changes ship as new changelog files only; existing changelogs are never edited, so upgrades are a restart.Scheduled jobs
Invoice generation, subscription cycle rollover, cancellation processing, and credit expiration run as in-process scheduled jobs (ShedLock-guarded, so multiple instances won’t double-fire). Default crons live inapplication.yaml under jobs:. Internal spend adds three hourly jobs:
vendorUsageSync (:15, re-pulls the last three days from each connected
vendor), spendBudget (:30, checks every budget), and outcomeSync (:45,
re-pulls merged PRs and completed issues).
Admin console
ui/ in the repo is a Next.js app covering the JWT admin surface — catalog,
customers, subscriptions, credits, invoices, events, and margin analytics.
npm run dev:ui runs it in development; for a production build, npm run build --workspace @tansohq/ui then npm run start --workspace @tansohq/ui.
One variable matters: TANSO_BASE_URL, the API address the Next.js server
proxies to (default http://localhost:8080). API calls go through that
same-origin proxy, so the API’s CORS configuration needs no changes for the
console.
Production checklist
- Change the seeded test account credentials and API key — they are public knowledge (they’re in this documentation).
- Set a strong, private
JWT_SECRET; rotating it invalidates sessions. - Set
APP_SECRETS_KEYand back it up outside the database — losing it means re-entering every stored credential. - The compose file ships Spring MVC and Hibernate logging at
INFOandshow_sqloff. Do not raise them in production: atDEBUG, request bodies — including keys you paste into the console — are written to the logs. - Put the API behind TLS; API keys are bearer credentials.
docker composeindeploy/is a starting point, not an HA architecture — the app is a standard Spring Boot container and runs anywhere containers run. Health probes are at/actuator/health/{liveness,readiness}.- Watch the logs for the first cycle close — billing bugs are cheapest the day you can still read the whole ledger by hand.