Skip to content

Commit 91efa2d

Browse files
committed
Wrote up the privacy policy and cleaned up some other files.
1 parent 45a4d4b commit 91efa2d

7 files changed

Lines changed: 698 additions & 25 deletions

File tree

LICENSE

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
Copyright (c) 2026 Nicholas Licalsi (nebulous-code)
2+
3+
# PolyForm Noncommercial License 1.0.0
4+
5+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
6+
7+
## Acceptance
8+
9+
In order to get any license under these terms, you must agree
10+
to them as both strict obligations and conditions to all
11+
your licenses.
12+
13+
## Copyright License
14+
15+
The licensor grants you a copyright license for the
16+
software to do everything you might do with the software
17+
that would otherwise infringe the licensor's copyright
18+
in it for any permitted purpose. However, you may
19+
only distribute the software according to [Distribution
20+
License](#distribution-license) and make changes or new works
21+
based on the software according to [Changes and New Works
22+
License](#changes-and-new-works-license).
23+
24+
## Distribution License
25+
26+
The licensor grants you an additional copyright license
27+
to distribute copies of the software. Your license
28+
to distribute covers distributing the software with
29+
changes and new works permitted by [Changes and New Works
30+
License](#changes-and-new-works-license).
31+
32+
## Notices
33+
34+
You must ensure that anyone who gets a copy of any part of
35+
the software from you also gets a copy of these terms or the
36+
URL for them above, as well as copies of any plain-text lines
37+
beginning with `Required Notice:` that the licensor provided
38+
with the software. For example:
39+
40+
> Required Notice: Copyright Nicholas Licalsi (nebulous-code)
41+
42+
## Changes and New Works License
43+
44+
The licensor grants you an additional copyright license to
45+
make changes and new works based on the software for any
46+
permitted purpose.
47+
48+
## Patent License
49+
50+
The licensor grants you a patent license for the software that
51+
covers patent claims the licensor can license, or becomes able
52+
to license, that you would infringe by using the software.
53+
54+
## Noncommercial Purposes
55+
56+
Any noncommercial purpose is a permitted purpose.
57+
58+
## Personal Uses
59+
60+
Personal use for research, experiment, and testing for
61+
the benefit of public knowledge, personal study, private
62+
entertainment, hobby projects, amateur pursuits, or religious
63+
observance, without any anticipated commercial application,
64+
is use for a permitted purpose.
65+
66+
## Noncommercial Organizations
67+
68+
Use by any charitable organization, educational institution,
69+
public research organization, public safety or health
70+
organization, environmental protection organization,
71+
or government institution is use for a permitted purpose
72+
regardless of the source of funding or obligations resulting
73+
from the funding.
74+
75+
## Fair Use
76+
77+
You may have "fair use" rights for the software under the
78+
law. These terms do not limit them.
79+
80+
## No Other Rights
81+
82+
These terms do not allow you to sublicense or transfer any of
83+
your licenses to anyone else, or prevent the licensor from
84+
granting licenses to anyone else. These terms do not imply
85+
any other licenses.
86+
87+
## Patent Defense
88+
89+
If you make any written claim that the software infringes or
90+
contributes to infringement of any patent, your patent license
91+
for the software granted under these terms ends immediately. If
92+
your company makes such a claim, your patent license ends
93+
immediately for work on behalf of your company.
94+
95+
## Violations
96+
97+
The first time you are notified in writing that you have
98+
violated any of these terms, or done anything with the software
99+
not covered by your licenses, your licenses can nonetheless
100+
continue if you come into full compliance with these terms,
101+
and take practical steps to correct past violations, within
102+
32 days of receiving notice. Otherwise, all your licenses
103+
end immediately.
104+
105+
## No Liability
106+
107+
***As far as the law allows, the software comes as is, without
108+
any warranty or condition, and the licensor will not be liable
109+
to you for any damages arising out of these terms or the use
110+
or nature of the software, under any kind of legal claim.***
111+
112+
## Definitions
113+
114+
The **licensor** is the individual or entity offering these
115+
terms, and the **software** is the software the licensor makes
116+
available under these terms.
117+
118+
**You** refers to the individual or entity agreeing to these
119+
terms.
120+
121+
**Your company** is any legal entity, sole proprietorship,
122+
or other kind of organization that you work for, plus all
123+
organizations that have control over, are under the control of,
124+
or are under common control with that organization. **Control**
125+
means ownership of substantially all the assets of an entity,
126+
or the power to direct its management and policies by vote,
127+
contract, or otherwise. Control can be direct or indirect.
128+
129+
**Your licenses** are all the licenses granted to you for the
130+
software under these terms.
131+
132+
**Use** means anything you do with the software requiring one
133+
of your licenses.

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,14 @@ npm run build
6262

6363
## Where to look first
6464

65-
- `docs/Cube_Practice_Design_Doc.md` — full product/spec
66-
- `docs/milestones/README.md` — milestone breakdown
67-
- `docs/milestones/01_auth_and_accounts.md` — current milestone's design + story list
68-
- `docs/TODO.md` — items on the user's plate (terms/privacy content, account provisioning, etc.)
65+
- `docs/ARCHITECTURE.md` — live as-built reference: schema, auth design, API contract, frontend routes
66+
- `docs/CHANGELOG.md` — what shipped when, milestone by milestone
67+
- `docs/TODO.md` — current hard-blocker list before launch
68+
- `docs/POST_MVP.md` — live backlog of work that's intentionally not in MVP
69+
- `docs/concepts/` — evergreen "why" docs (SM-2-vs-Anki, OLL case reference)
70+
- `docs/milestones/` — per-phase design docs (00 is the original spec; 01–07 are the MVP build)
71+
- `docs/policies/` — Privacy Policy and Terms of Service
72+
73+
## License
74+
75+
Source-available under the **PolyForm Noncommercial License 1.0.0** — see [LICENSE](LICENSE) for the full text. In short: anyone may read, run, modify, and contribute to this code for any noncommercial purpose. Commercial use of the code itself (including standing up a competing hosted version for resale) is reserved to the maintainer. The hosted service at cube.nebulouscode.com is governed separately by the [Terms of Service](docs/policies/terms_of_service.md).

docs/POST_MVP.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Post-MVP backlog
2+
3+
Live list of work that is intentionally *not* in MVP. Originally lived
4+
inside `milestones/00_initial_design_doc.md` §1, but the design doc is a
5+
frozen historical artifact and this list keeps growing — so it lives
6+
here now.
7+
8+
Each entry is "what + why-it's-deferred + rough shape if obvious."
9+
Nothing here is committed; this is the queue we pull from when deciding
10+
what to build next, not a roadmap with dates.
11+
12+
## Scope expansion
13+
14+
- PLL, F2L, and other solve-stage expansion.
15+
- Other cube types (4×4, Megaminx, etc.).
16+
- Public case browser (no login required).
17+
18+
## Product surface
19+
20+
- Stats over time and progress graphs (a placeholder/skeleton view
21+
ships in MVP so users know it's coming).
22+
- Admin panel.
23+
- Dark mode.
24+
- Additional public-facing marketing pages (features, pricing, FAQ,
25+
etc.) beyond the M5 landing page.
26+
- Full accessibility audit — screen-reader walkthrough, ARIA live
27+
regions, complete WCAG AA review. M5 ships a basic pass: keyboard
28+
nav, focus rings, form labels, spot-check contrast.
29+
- **Avatar.** Allow users to pick an OLL case as their avatar in the
30+
style of the logo.
31+
32+
## Study mechanics
33+
34+
- **Per-user timezone + local-midnight rollover** for streak/due-date
35+
comparisons. MVP uses server UTC date for "today" — a user in PST
36+
sees streaks tick at 5 PM Pacific (00:00 UTC). Post-MVP: store
37+
`users.timezone`, roll over at user-local midnight. Two reviews near
38+
UTC midnight currently can fall on different "today" values; that
39+
goes away with per-user rollover.
40+
- **Free-study filters: disable chips with no remaining matches.**
41+
Today, picking "L" as the primary shape leaves every tag chip enabled
42+
even though tags like "knight_move" don't intersect with L cases —
43+
the user discovers this only by hitting "0 cases match". Post-MVP:
44+
gray out / hide chips whose addition would leave the result set
45+
empty given the current filter state. Same treatment for tags/state
46+
interactions.
47+
48+
## Account / data
49+
50+
- **"Download my data" / data export before account deletion.**
51+
Deferred from M7 (`milestones/07_delete_account.md`). MVP delete is
52+
straight hard-delete; a JSON dump endpoint + Settings-side download
53+
flow can layer in once explicit user demand surfaces.
54+
- **Guest mode "Discard guest data" Settings entry.** Deferred from M7.
55+
Trivially `clearGuestState()`, but the UX (confirmation pane, warning
56+
copy) deserves its own design pass — the existing M7 deletion flow
57+
is account-scoped and doesn't apply.
58+
59+
## Email / notifications
60+
61+
- **Email reminders.** Opt-in daily/weekly nudge when the user has
62+
cards due. Needs a `users.reminder_preference` enum (off / daily /
63+
weekly), a per-user "last reminder sent" timestamp, and a worker pass
64+
that runs on a schedule. Resend integration already in place from
65+
M1 — wiring is the easy part; the design call is cadence, copy, and
66+
unsubscribe-link semantics.
67+
- **Easier-to-copy verification / reset codes in email.** Today the
68+
6-digit code sits inside an HTML paragraph; on mobile the user has to
69+
long-press and trim whitespace. Wrap the code in a `<code>`/monospace
70+
box with generous padding so a single tap selects the whole code, or
71+
include a Markdown-style fenced block in the plaintext email body.
72+
Trivial template change once the design lands.
73+
74+
## Monetization
75+
76+
- **Paid email subscription / premium tier.** Possible monetization
77+
path. Would need: subscription state on `users`, a billing provider
78+
(Stripe likely), gated features (TBD — possibly stats over time,
79+
additional puzzle types, or dark mode), and a customer-portal flow
80+
for plan changes / cancellations. Discovery work; not committed.
81+
82+
## Infrastructure / ops
83+
84+
- **Cold-start UX safety net.** Render free tier spins down after ~15
85+
min idle; the next request stalls 20–60s while the dyno wakes. The
86+
splash screen handles the boot case, but in-session API calls
87+
(grade, reveal, etc.) currently look frozen. Two layered ideas:
88+
1. **Top progress bar (NProgress-style).** ~2px bar at the top of
89+
the viewport, animates while any axios request is in flight.
90+
Axios interceptors increment/decrement a counter; bar component
91+
watches it. Calibrated so it's invisible on a 200ms request and
92+
visibly creeps on a 30s one. Zero visual cost on fast requests,
93+
immediate signal on slow ones.
94+
2. **Subtle full-screen overlay after ~8 seconds.** When a request
95+
has been in flight that long, fade in a 30–50% black overlay
96+
with a small note: "still working — the server takes up to a
97+
minute to wake up after a quiet period." Dismisses on response.
98+
Threshold tuned to fire effectively only on cold-start, not on
99+
slow mobile networks.
100+
101+
Either of these makes the spin-down tolerable. The "real fix" is a
102+
paid Render tier (~$5/mo) once the project has any monetization or
103+
donation path.
104+
105+
## Privacy / compliance
106+
107+
A grab-bag of ways to tighten the app's privacy posture and make the
108+
forthcoming Privacy Policy easier to defend. Not all of these will get
109+
done — they're noted so the decision to skip any one of them is
110+
deliberate rather than accidental.
111+
112+
- **Self-host the WOFF2 font files instead of loading from Google
113+
Fonts.** Today `frontend/src/assets/tokens.css` imports Newsreader,
114+
Inter Tight, and JetBrains Mono from `fonts.googleapis.com`. That
115+
means Google sees every visitor's IP on every page load, which
116+
contradicts the "we don't share data with Google beyond reCAPTCHA"
117+
posture in the privacy policy. Fix is one afternoon of work:
118+
download the WOFF2s, drop them under `frontend/public/fonts/`,
119+
replace the `@import` with `@font-face` blocks pointing at the local
120+
paths. SIL OFL allows redistribution; no attribution required beyond
121+
the license file itself. Bonus: faster first paint, since the
122+
browser stops doing a cross-origin DNS lookup + handshake for
123+
fonts.googleapis.com on every cold visit.
124+
- **Replace Google reCAPTCHA v3 with Cloudflare Turnstile.** Today
125+
registration sends a token derived from the user's browser
126+
fingerprint + IP to Google. Turnstile is Cloudflare's equivalent
127+
with a no-tracking design (no third-party cookies, no behavioral
128+
fingerprinting marketed back to ads) and we already proxy through
129+
Cloudflare anyway, so the subprocessor count actually drops by one.
130+
Drop-in swap on the registration form + one env var on the backend.
131+
- **Hash the email in `account_deletions` instead of storing
132+
plaintext.** Audit table currently stores `email` + `deleted_at` so
133+
we can support re-registration limits and abuse investigation. A
134+
one-way hash (with a server-side pepper) covers both use cases —
135+
re-register flow hashes the candidate email and looks for a match —
136+
without keeping recognizable PII after the user asks to be deleted.
137+
The downside is we lose human-readable rows for abuse review; the
138+
upside is the audit table stops being a deletion-survives-deletion
139+
problem.
140+
- **Add error tracking thoughtfully (self-hosted GlitchTip or Sentry)
141+
or commit publicly to not adding it.** Right now we have neither,
142+
which means real production errors disappear silently. Path A:
143+
self-host GlitchTip on a $5 box so error payloads never leave our
144+
infra; we'd need to scrub user emails / IDs from breadcrumbs before
145+
ingest. Path B: write "no error tracking is wired up" into the
146+
privacy policy and live with the operational cost. Either is
147+
defensible; the current "vaguely meaning to add Sentry" middle state
148+
is not.
149+
- **Reduce session cookie lifetime / add idle expiry.** Current JWT is
150+
a 30-day absolute lifetime with no server-side idle timeout. Options:
151+
drop absolute lifetime to 7 days, add a 14-day idle timeout enforced
152+
on the server (`sessions.last_seen_at`, refreshed lazily on each
153+
authenticated request), or rolling expiry that re-issues the JWT on
154+
active use. Trade-off is "log me back in" friction vs. the size of
155+
the steal-cookie blast radius.
156+
- **`SECURITY.md` + responsible-disclosure email.** Standard hygiene
157+
for any public-facing app. A `SECURITY.md` at the repo root pointing
158+
at a `security@nebulouscode.com` (or similar) tells researchers where
159+
to send a quiet heads-up rather than opening a public GitHub issue.
160+
Coupled with a one-line acknowledgement section that names them once
161+
the patch ships, this is one of the cheapest professionalism wins.
162+
- **`/.well-known/privacy.txt` and/or Global Privacy Control signal.**
163+
Two adjacent ideas: ship a `privacy.txt` at a well-known path
164+
pointing at the policy, contact, and jurisdiction, *and/or* honor
165+
the `Sec-GPC: 1` browser header by treating it as a do-not-sell /
166+
do-not-share signal at the API layer. We don't sell data today so
167+
GPC is mostly a posture statement, but it's also future-proofing
168+
against ever adding analytics.
169+
- **Switch transactional email off Resend.** Resend is fine but it's
170+
another US-based subprocessor that sees every user's email address +
171+
the contents of verification/reset emails. Options: a self-hosted
172+
MTA (Postal, Maddy, or even raw Postfix on a $5 box), or a more
173+
privacy-aligned provider like Postmark or Tutanota's transactional
174+
product. The self-host path is meaningfully more work (DNS, DKIM,
175+
SPF, DMARC, deliverability monitoring) and probably only worth it
176+
alongside the EU residency work below.
177+
- **Stop using JWTs for sessions; use opaque session tokens.** JWTs are
178+
fine in our shape (we already track a `sessions` row with a token
179+
hash so sign-out-all works), but the JWT itself is structurally a
180+
bearer credential that's hard to revoke server-side without the
181+
parallel sessions table. Switching to opaque random tokens (just the
182+
`sessions.token_hash` UUID, validated by DB lookup on each request)
183+
collapses the design — one source of truth for "is this session
184+
alive", no JWT-secret-rotation problem, no `exp`/`iat` claim drift.
185+
Cost is one DB hit per authenticated request (already happening for
186+
user lookup anyway).
187+
- **Region-pinned data residency option.** Only worth doing if we
188+
start to pursue EU users seriously. Would mean: an EU-region Neon
189+
branch, an EU-region Render service, EU-aligned email subprocessor,
190+
and a routing layer (Cloudflare Workers or similar) that pins a
191+
signed-up EU user's traffic to the EU stack. Big project; flagged
192+
here so future-us knows the cost of saying "we serve the EU" out
193+
loud.
194+
195+
## Testing
196+
197+
- **HTTP integration tests for route handlers.** Today we unit-test
198+
the lib functions each handler delegates to (high coverage on the
199+
business logic) but not the HTTP plumbing — auth gating, request
200+
validation, JSON response shape, status codes, cookie attachment.
201+
The handler-layer files (`routes/*`, `auth/extractor.rs`,
202+
`auth/session.rs`, `error.rs`'s `IntoResponse`) are excluded from the
203+
95% coverage gate for that reason. Adding tower-based in-process
204+
integration tests (spin up the Axum app, send real `Request`s via
205+
`tower::ServiceExt::oneshot`, assert responses) would close the gap.
206+
Cost: ~30–50% more test code, slower CI; benefit: catches
207+
handler-level regressions (renamed JSON fields, dropped auth gates,
208+
wrong status codes) that lib-level tests can't see. Worth doing once
209+
the project has more than one developer, or on the first regression
210+
that the lib tests miss.
211+
212+
---
213+
214+
## Out of scope (MVP)
215+
216+
These were called out at design time as explicitly *not* the direction
217+
of the product, not just "not yet":
218+
219+
- Native mobile app.
220+
- Social features.
221+
- Sharing custom decks between users.

docs/TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Things that need a human decision, content, design, or external action — items
99
- [ ] **Terms of Service content.** Static page at `/terms` ships with placeholder text. Linked from the registration legal footer and Settings → About.
1010
- [ ] **Privacy Policy content.** Static page at `/privacy` ships with placeholder text. Same link surfaces as Terms.
1111
- [ ] **OLL case numbering universality.** Confirm the 1–57 numbering used in `data.jsx` (and seeded into `cases.case_number`) matches the cubing-community convention. ~30-min check; renumbering after launch breaks any URL or progress data already keyed on case number.
12+
- [ ] Update fonts to be stored and served locally.
1213

1314
## Cosmetic but visible to first-time users
1415

0 commit comments

Comments
 (0)