mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
Checkout, the customer portal, and the webhook that moves a team's plan. Nothing money-shaped is stored. No price, no currency, no card detail — a subscription's existence and its state are the whole of what crosses back, because they are the only two facts the product needs and anything more would be a second copy of a record somebody else is authoritative for. Currency is deliberately not ours to hold. A product carries a price per currency on their side and the customer's location picks one at checkout, so there is no figure in this API that could drift from the one somebody is charged. The team id travels as the customer's external id, which keeps the mapping on their side rather than putting a foreign primary key in our schema. Access follows their state, and the interesting cases are where that is not the same as "paying right now". Cancelling keeps the plan: they paid to the end of the period and turning them off when they click it takes something they bought. A failed card keeps it too, because a retry that ends in payment should not have cost them access in the middle. Only a revoked subscription takes it away, which is the one moment nothing is left that was paid for. An event we do not recognise changes nothing at all — new types are added by people who do not know what we do with them, and a default that moved a plan would eventually cancel an account nobody cancelled. The webhook is the only route here no session protects, because its caller has no account and never will. A signature over the raw body stands in for one, and it is checked before the body is looked at — a body that has been parsed and re-serialized is not the body that was signed. With no secret configured it refuses everything rather than accepting anything, since otherwise knowing the URL would be enough to set somebody's plan. Note also what is absent: no route sets a plan, so there is no endpoint for granting yourself a subscription. The product is written down as a definition with a script rather than clicked into a dashboard, because the two environments are separate servers and nothing made in one can be moved to the other. Promoting it is running the same script with the other token, which is the only version of that which cannot drift. It writes nothing without --apply and refuses to add a second product with a name already taken.
48 lines
2.2 KiB
Plaintext
48 lines
2.2 KiB
Plaintext
# Copy to `.env` before `docker compose up`. Compose reads every credential
|
|
# from here and has no defaults of its own — it refuses to start naming the
|
|
# variable it wanted rather than falling back to a value that would be public.
|
|
|
|
# The local database. Throwaway values are fine; these three are what compose
|
|
# creates the container with and what it builds DATABASE_URL from.
|
|
POSTGRES_USER=postgres
|
|
POSTGRES_PASSWORD=postgres
|
|
POSTGRES_DB=nestri
|
|
|
|
# For anything run outside a container — `bun dev`, `bun run db:migrate`.
|
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/nestri
|
|
|
|
# The database the tests run against. Required — DB-backed tests refuse to run
|
|
# rather than fall back to a database nobody named.
|
|
#
|
|
# **Point it at the same database as DATABASE_URL above.** "Isolated" means
|
|
# isolated from anything you care about, not isolated from DATABASE_URL: route
|
|
# tests reach the database through the app and core tests reach it directly, so
|
|
# two different values put the fixtures in one database and the assertions in
|
|
# the other. That fails around forty tests, in neither half's own code, with
|
|
# nothing in the output pointing at this line.
|
|
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/nestri
|
|
|
|
# The issuer's public URL — the address a token's `iss` claim will carry.
|
|
AUTH_ISSUER_URL=http://localhost:1337
|
|
# Where to reach the issuer, if that is not where it lives. Unset unless the
|
|
# public name is unroutable from where the API runs; docker compose sets it.
|
|
AUTH_INTERNAL_URL=
|
|
|
|
# Mail delivery. All three together, or none of them plus EMAIL_DEV_LOG=true,
|
|
# which prints sign-in codes to the log instead of sending them. Printing them
|
|
# is a local-development convenience and nothing else.
|
|
EMAIL_SEND_URL=
|
|
EMAIL_API_KEY=
|
|
EMAIL_FROM=
|
|
EMAIL_DEV_LOG=true
|
|
|
|
# Billing. The provider's sandbox and production are separate servers with
|
|
# separate data, so a token from one is refused by the other and a product id
|
|
# from one means nothing to it. `POLAR_SERVER` says which you are talking to.
|
|
POLAR_SERVER=sandbox
|
|
POLAR_ACCESS_TOKEN=
|
|
POLAR_PRODUCT_ID=
|
|
# Signs every webhook delivery. Without it the webhook route refuses everything,
|
|
# on purpose: nothing else stands in front of it.
|
|
POLAR_WEBHOOK_SECRET=
|