Privacy policy

For toy-shop-prod. Last updated NOT FILLED IN: the date you adopt this.

1. Who is responsible for your data

The controller of the personal data described here is NOT FILLED IN: the full legal name of the entity that operates toy-shop-prod, registered in NOT FILLED IN: the country and, if it has one, the company number, at NOT FILLED IN: the registered postal address.

Questions about this policy, and any request under section 6, go to NOT FILLED IN: the contact address a privacy policy has to carry, which should be a mailbox someone reads.

If payments are taken through a merchant of record, that provider is the controller for the payment itself and this service is not. Which of the two applies is NOT FILLED IN: which payment provider is in force, and whether it acts as merchant of record, and section 3 names the provider this deployment is configured for.

2. What this policy covers

The service at toy-shop-prod and nothing else. It does not cover a site you reached this one from, or a service you are sent to from here.

3. What personal data this service holds, and why

The table below is not written by hand. It is collected from the components installed in this deployment, each of which declares what personal data it touches next to the code that touches it, so it describes this product rather than a template's guess about it. The last column is the file you or anyone else can read to check the row, relative to this project's src/app/ - so every claim below can be refused by reading forty lines of code.

Data Where it is held Why Who else receives it What deletes it Proof
Email address the `users` table, column `email`, and `user_identities.email` it is the account identifier, it is what a sign-in is checked against, and it is the address every password-reset, sign-in and verification link is sent to whichever mail transport ADAPTER_EMAIL names. The shipped default is `console`, which sends nothing anywhere and is refused in production, so a deployed product is on `resend` - a third party in the United States - or on `smtp`, pointing at a relay the operator chooses erase_subject in core/erasure.py destroys the row, which is what makes every actor_id in the audit trail resolve to nobody. DELETE /auth/me is what asks for it, re-authenticated - AUTH-004 auth/models/user.py, auth/routes/email_auth.py
Password, as an argon2 hash the `users` table, column `password_hash` it is how a sign-in is checked without the password itself being kept. The password is never stored, never logged and never returned nobody outside this service with the row it sits on, by erase_subject auth/services/password.py
That you asked to be erased, and when your rows are due to go the `users` table, column `erasure_due_at` it is the date a nonzero ERASURE_GRACE_DAYS produced, and it is the only thing that says an account is on its way out rather than merely deactivated. Null for everybody who has not asked, which is everybody at the shipped window of zero - there the request erases the row instead of marking it nobody outside this service with the row it sits on. It exists in order to be erased: the sweep in auth/tasks.py selects on it, calls erase_subject, and the row goes auth/services/account_erasure.py, auth/tasks.py
A third-party sign-in, as the provider's own subject identifier the `user_identities` table: provider, subject, email, verified flag it is what links a Google sign-in to an account here, so signing in twice with the same Google account reaches the same account Google, when a person chooses to sign in that way. Google sees that the sign-in happened and returns the identifier and address recorded here by the cascade from users.id, when erase_subject deletes that row auth/models/identity.py, auth/adapters/identity/google.py
Single-use sign-in and password-reset links, as digests the `email_tokens` table, which holds a digest and never the token it is what makes a mailed link work once and then stop working, and what enforces its expiry nobody outside this service by the cascade from users.id. Short of that, auth.email_tokens.purge deletes them: a token whose expiry is more than AUTH_EMAIL_TOKEN_RETENTION_DAYS past is gone, thirty days as shipped. This entry said nothing pruned them, which stopped being true when the sweep landed and was still written here - CORE-039 found it auth/models/email_token.py
Your signed-in devices: when each signed in, the browser or app it reported being, and the address it connected from the `sessions` table: user_id, issued_at, expires_at, revoked_at, user_agent and ip. The refresh token itself is never stored - the column holds a SHA-256 digest of it it is what keeps you signed in on a device for longer than an access token lives, and it is the only thing that can end a sign-in early: signing out revokes the row, and a replayed refresh token revokes it too. The user agent and the address are there so a list of your own sessions can tell you which device each one is nobody outside this service by the cascade from users.id, when erase_subject deletes that row - which is also what ends every session the account has. Short of that, swept: auth.sessions.purge deletes rows more than AUTH_SESSION_RETENTION_DAYS past expires_at, declared in core/retention.py. A live session is never swept, because the window is measured from when it stopped being renewable auth/models/session.py, auth/services/session.py
Email address, sent to the payment provider the provider's records, not this service. `billing_customers` keeps the provider's customer id against a user id and nothing else about the person it is how a subscription here is matched to a customer there. Paddle offers no other lookup key and is keyed on the address Stripe or Paddle, whichever ADAPTER_BILLING names. The shipped default is `fake`, which reaches no network at all. Where Paddle is in force it is the merchant of record, which makes it the seller of your purchase and changes who is responsible for the payment data nothing here, because nothing here holds it. An erasure forwards the request to the provider, which is a documented step rather than a silent omission, and the provider's own policy decides the rest - COMP-036 core/adapters/billing/stripe.py, core/adapters/billing/paddle.py
The link between a person here and a customer at the provider the `billing_customers` table: a provider name, the provider's customer id, and the user id. Nothing else about the person it is what a webhook is matched against, and what makes an invoice attributable years after the account is gone nobody outside this service the row is kept and `user_id` is set to null by erase_subject, which is what detaches the invoice from the person. What is left points at the provider's customer and at nobody here - COMP-036 billing/models/billing.py
Subscription state the `subscriptions` table: plan, status, period dates it is what decides whether a paid route answers, without asking the provider on every request nobody outside this service nothing deletes these rows, and that is the decision rather than an omission. They hold no name, no address and no card, so they survive the person without naming them once billing_customers.user_id is null billing/models/billing.py
Payment attempts and their amounts the `billing_charges` table: amount, currency, plan, the provider's own reference, and the idempotency key of the attempt it is the record on our side that money was asked for, which a request that timed out after the card was charged would otherwise lose nobody outside this service nothing deletes these rows. They are the invoices, and erasing them is the unlawful direction adr-006 names billing/models/billing.py
Card and bank details nowhere in this service. They are entered on the provider's own page and never reach this application recorded so that the absence is on the record rather than assumed. No schema or route under billing/ has a card field the payment provider, directly from the payer's browser not applicable - nothing is held here to delete billing/routes, billing/schemas
IP address, on every request the application's log stream, in the `client_ip` field of each line it is what a rate limit counts against, and what makes a log usable for finding out what happened whatever collects the logs of wherever this is deployed. On the one walked deploy path that is fly.io nothing here, and nothing here can. Log retention is a setting on whatever collects them, which is why core/retention.py names the log stream as one of the three things that hold personal data outside this database and have no period this repository can set - CORE-039 core/middleware/request_log.py, core/middleware/client_ip.py
Actor identifier and free-form context, in the audit trail the `audit_log` table: `actor_id`, `request_id`, and a JSON `context` column whose every key is declared in `core/audit_context.py` with a reason why it holds no personal data. No address: CORE-047 removed `ip_address`, and `request_id` is what joins a row to the log line that has one it is the record of who did what, which exists in order to survive the thing it describes nobody outside this service nothing, and that is by design rather than by oversight: an entry carries no foreign key so that it outlives the row it is about. Erasing the person destroys the `users` row, which is the only thing that resolves `actor_id`, so the trail keeps saying what happened and stops saying who. Nothing prunes it either, and that is now a decision with a reason rather than a silence: core/retention.py declares `audit_log` exempt, because one row in it - `account.erased` - is the ledger a restore replays, so a sweep of any window would delete the record of every erasure older than it. CC.07.03 asks for a number here and the owner has not set one - CORE-039 core/models/audit.py, core/audit.py
A child's first name the `shop_children` table, column `display_name`, capped at 40 characters. One name, typed by the shopkeeper. No surname, no age, no date of birth, no email address, no photograph and no password - there is no child account to attach any of those to there are five wishlists and five balances, and the shopkeeper has to be able to tell whose is whose. A list nobody can attribute is not a list. It is shown on the child's own wishlist page and on the shopkeeper's page, and nowhere else nobody outside this service the row is deleted by the shopkeeper, and the wishlist and credit ledger go with it by ON DELETE CASCADE. There is no account erasure path for a child because there is no child account: a child is not a subject this application can authenticate, so `DELETE /auth/me` never reaches these rows and the shopkeeper deleting the row is the whole mechanism shop/models/child.py, shop/routes/shopkeeper.py, shop/routes/wishlist.py
The fingerprint of a child's private wishlist link the `shop_children` table, column `link_fingerprint`: sha256 of the token, hex. **The token itself is never stored.** It is printed once when the link is minted and cannot be recovered afterwards, only replaced it is the whole of the authorization for a wishlist page. Holding the token lets you edit one list, and that is why there is no login for a child and no identity attached to the link nobody outside this service with the row. Turning a leaked link off without deleting anything is `active = false`, which keeps the wishlist and the balance and stops the URL working shop/children.py, shop/models/child.py
What a child said they want, and how much they want it the `shop_wishlist_items` table: the catalogue slug and a rank, one row per wanted thing. No free text - a child cannot type anything into this shop, so there is no field here that could hold something nobody predicted it is the product. Ranked rather than ticked because which one to buy first is the question the shopkeeper actually has nobody outside this service by the cascade from shop_children.id when the child row is deleted shop/models/wishlist.py, shop/wishlists.py
A child's credit balance and every movement of it the `shop_credit_entries` table: an amount in agorot, the order it paid for if it was a spend, and a short reason the shopkeeper typed. The balance is the sum of the rows and is not stored anywhere so that a child can be given pocket money to spend here and can see where it went. The reason field is what a movement is explained by nobody outside this service by the cascade from shop_children.id. This is not an invoice: no money moved through it, it is a number the shopkeeper put on a family account, and there is no obligation to keep it once the child it belongs to is gone shop/models/credit.py, shop/credits.py
Who an order is going to, and where the `shop_orders` table: `recipient_name`, `address`, `phone` and `note`, all free text the buyer typed. This is the most sensitive thing this application holds - the recipient is usually a child, so it is a named minor's street address fulfilment is manual. The shopkeeper reads this off a page and writes it on a parcel. There is no warehouse integration and the address is sent to nobody nobody outside this service by the cascade from users.id: shop_orders.buyer_id is ON DELETE CASCADE, so erase_subject's DELETE FROM users destroys the order and its lines. That is a deliberate departure from adr-006's invoice exemption and shop/models/order.py argues it: this shop takes payments in the provider's test mode only, so there is no invoice to keep, and a minor's address outliving the account that supplied it is the worse of the two available mistakes. If this shop ever charges a real card, that decision is revisited in the same commit shop/models/order.py, shop/routes/orders.py, shop/routes/shopkeeper.py
What a person bought, when, and for how much the `shop_orders` and `shop_order_items` tables: the buyer's id, the total in agorot, and one line per product with the name and unit price as they were on the day the buyer sees their own orders and the shopkeeper sees what to go and buy. Every read of an order is WHERE buyer_id = the subject in the verified token, except the shopkeeper's list, which is one address named in SHOP_KEEPER_EMAIL nobody outside this service with the order rows, by the same cascade shop/models/order.py, shop/routes/orders.py, shop/config.py
The link between a buyer here and a customer at the payment provider shop_orders.provider and shop_orders.provider_customer_id, alongside the same link app/billing keeps in billing_customers a webhook delivery names its customer in the provider's vocabulary and not in ours, so this column is what a signed delivery is matched against before an order is recorded as paid the payment provider named by ADAPTER_BILLING. The configured default here is `fake`, which reaches no network at all and moves no money; when it moves to Stripe it moves to Stripe **test mode** with the order row. The corresponding billing_customers row is not this product's and is kept with user_id nulled, which is the billing component's own declaration shop/orders.py
Card and bank details nowhere in this shop. They are entered on the provider's own page and never reach this application recorded so that the absence is on the record rather than assumed. No schema, form or route under shop/ has a card field, and the only thing this shop ever sends a buyer is a URL the payment provider, directly from the payer's browser not applicable - nothing is held here to delete shop/routes/orders.py, shop/templates/
Anything at all about somebody who is only browsing nowhere. The catalogue and the franchise filter are served to anybody with no cookie, no session, no account and no row written. A visitor who never places an order leaves nothing behind in this database recorded because it is the decision the whole product is arranged around and an absence nobody wrote down is an absence nobody can check. Everyone browsing this shop is a minor; collecting an email address so a nine-year-old can look at a plush is a liability with no product reason behind it nobody outside this service not applicable - nothing is held shop/routes/catalogue.py, docs/BRIEF.md
A session cookie, named `session`, carrying the sign-in token the visitor's own browser. This service keeps no session table it is what keeps a person signed in from one request to the next nobody outside this service signing out clears it, and it expires by itself when the token does. An erasure cannot reach it: there is no browser in the room when it runs, which is why the token's own lifetime is the bound - AUTH-007 webauth/session.py

23 declaration(s), from auth, billing, core, shop, webauth.

4. Cookies

One cookie, named session. It carries the token that keeps you signed in, it is set only when you sign in, and it is removed when you sign out. It is HttpOnly, so no script on the page can read it, and SameSite=strict, so your browser does not send it to this service from anywhere else. It expires on its own after the sign-in token does.

There is no analytics cookie, no advertising cookie and no third-party script on any page of this service. If you add one, this section stops being true and a consent banner becomes a separate question.

5. How long data is kept

The "what deletes it" column above is the honest answer per item, and where it says nothing deletes something, nothing deletes it.

The retention period this service commits to is NOT FILLED IN: how long you keep account data after an account is closed, and how long you keep logs and the audit trail. A period stated here has to be one the code actually enforces, so do not write a number before something deletes on it.

6. Your rights

Depending on where you live, you may have the right to ask for a copy of your data, to have it corrected, to have it deleted, to object to some uses of it, and to complain to a supervisory authority. Requests go to the address in section 1.

What this service cannot currently do on its own: there is no button that deletes an account, and the audit trail records actions against an identifier and an IP address and nothing removes those. A deletion request is therefore handled by hand today. Do not promise erasure on a timetable this deployment cannot keep - state the one it can.

7. Where data is processed

Data is held by NOT FILLED IN: your hosting provider, and the region its database runs in, and by the providers named in section 3. Whether a transfer leaves your users' jurisdiction, and what makes such a transfer lawful if it does, is NOT FILLED IN: the transfer basis, and whether a data processing agreement is needed with each provider named above.

8. Children

This service is not directed at children, and the minimum age to hold an account is NOT FILLED IN: the minimum age, which is a different number in different places.

9. Changes to this policy

Changes are published on this page, and the date at the top is the date of the version in force.