Files
netris-nestri/.env.example
Wanjohi 757ff79233 feat(core,api): take payment, and let the provider decide who is paid up
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.
2026-09-19 00:57:57 +03:00

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=