santa-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev b8b257a837b1
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone c7891873549c9ca1004894b3
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
AGENTS.md
# Santa

Santa is a free Secret Santa generator with exclusions, wish lists and private reveals, and a free alternative to Elfster with no ads. It is an OpenApp in the OpenApps free apps store (https://openapps.free/), owned by the OpenApps team.

## What it promises

- Free, with no limit on exchanges, no ads and no paid tier. Never add a cap, an upsell or tracking. An exchange holds up to 99 people (the draw writes the exchange and every match in one batch, and a batch holds at most 100 documents).
- No sign-up. Organizers and people are guest accounts created in the browser.
- The draw is fair: nobody draws themselves or anyone they are kept apart from, and every valid draw is equally likely (shuffle and keep the first shuffle that follows every rule; a random-order search only when shuffling finds none). When no draw can exist, it names the people who are stuck. See `drawNames` in `functions/lib/santa.ts`.
- By default nobody, including the organizer, can see who gives to whom. The organizer can choose to see the list before the draw, and every person's page then says so. This is enforced by the policy, not just the UI (see below).
- Every device that opens a person's private page is listed on that page, so a peek shows.
- Honest limits, said on the landing page: Santa sends no emails or texts (it writes messages people send themselves, with mailto and sms shortcuts); no gift shop or shipping addresses; no accounts.
- AI assistants can do everything a person can through the public API (`functions/api.ts`), documented in `public/llms.txt` and `public/openapi.json`. Keep both true whenever the API changes.

## How it is built

- `index.html` is the static landing page that search engines and AI crawlers read; `public/index.md` is the same page as plain markdown. Keep the copy, comparison, FAQ and JSON-LD in sync with what the app does, and keep competitor claims to what was checked, dated.
- `src/` is the React app. `/` mounts the exchange maker (`Create.tsx`) and "Your pages" (`Yours.tsx`, from localStorage) into the landing; the landing never loads the Bounded SDK. `/x/<id>` is the join link (`Join.tsx`), `/x/<id>/admin` the organizer's page (`Admin.tsx`, `#k=<ownerKey>` claims it), `/x/<id>/p/<personId>` one person's private page (`Person.tsx`, `#k=<personKey>` opens it after asking "is this you?").
- `functions/lib/santa.ts` holds the logic shared by the app and the API: the draw, input checks, the messages to copy, dates and the calendar file.
- `functions/lib/config.ts` holds the app id and the site URL used in links (`https://santa-free.openapps.xyz`).
- `policy.json` holds the data rules:
  - `exchanges/<id>` and its names are readable after a join (`exchanges/<id>/joins/<userId>`), which needs only the id. Owners are `exchanges/<id>/owners/<userId>`; the admin key lives in `exchangekeys/<id>`, readable only by the public API function.
  - Each person has a key in `personkeys/<personId>` (readable only by the API), made by whoever adds them: the organizer, or the person themselves from the join link. A device holds a person when it wrote `personclaims/<personId>/c/<userId>` with the right key, and that claim only counts together with an open record `exchanges/<id>/opens/<personId>_<userId>`, which everyone in the exchange can read. That is what lists every device on a page. Only the public API's own record says `via: "api"`; every API caller shares it, so the API moves its `at` to each call and the page shows the latest.
  - Matches are `exchanges/<id>/matches/<personId>` ({to, round}). Only the public API principal can write them, so the draw runs in `functions/api.ts` and never in an organizer's browser. A match is readable by a device holding that person, by owners only when `organizerSees` is true, and by everyone in the exchange after `revealAt`; always only for the current round. `organizerSees` can change only while the exchange is open. A reveal is always set in the future; after the draw it can only move later or be turned off, and once it passes the draw is final (no undo, no redraw, no reveal change). Every draw adds one to `round`, and matches are written only in that batch.
  - A Secret Santa proves who they are for each round with `exchanges/<id>/santas/<to>/s/<userId>`, checked against their claim and their match. That proof opens the giftee's wish list (`wishes/<personId>`) and lets them ask anonymous questions (`asks/<to>/q/<qid>`). Every reader sees who wrote a document (`_createdBy`), and the open records map devices to people, so only the public API writes questions: a browser leaves its question in `askrequests/<qid>` (readable only by the API) and POSTs `/questions/<qid>`. Only the giftee answers.
  - `keepapart/<id>` (pairs as JSON) is readable only by owners, since last year's pairs would give away last year's secret.
- The policy mentions `@const.BOUNDED_PUBLIC_PRINCIPAL_API`, so a fresh app needs the two-phase deploy described in the store's app guide.
- `policy-tests/` checks every write rule (run `bounded tests run`).

## Working on it

`npm install && npm run build` typechecks and builds `dist/`. Ship changes through previews and releases as your runtime instructions describe; the app is an OpenApp, so nobody deploys it directly.

Check a change by making an exchange with your own name and three others, keeping a couple apart, joining from the link in a second browser and writing a wish list, drawing, opening a private link in a third browser, asking the giftee a question and answering it from the giftee's page, opening someone else's link from the organizer's browser (their page should list another device), undoing and drawing again with "I want to see it" and a reveal, and calling the API with curl (including a wrong owner key, a wrong person key and an impossible set of pairs).

## Voice

Plain words, short sentences, no jargon, no em dashes. Say what happens, not how clever it is.

Change history

Point OpenApps links to openapps.free
b8b257a837 · 18 hours ago
Bind agent address in policy constants
9a72d0200b · 20 hours ago
Use functions.openapps.xyz alias, add OpenApp contribution strip
60038fa63d · 20 hours ago
Bounded source sync
a2e0247232 · 1 day ago
Bounded source sync
c84c736f8b · 1 day ago
Bounded source sync
3a54a80cdc · 1 day ago
Bounded source sync
2eed41b4de · 1 day ago