Skip to content

jphein/starcharts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

68 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

                              .          ✦
                  ·       .            .       ·
              ✦          .       ·         .
                   .          ✦         .       ✦
              ·         .           ·          .
                  .         ·           ✦         .
                                  S T A R C H A R T S
                  .          ✦         .          ✦
              ·       .          ·            .
                  .       ·          .       ·
                                                    ✦

A shared night sky for the people you love — a quest tracker that doesn't feel like one. Give each other stars for meaningful acts. When the sky fills, the reward unlocks, and the chart becomes a constellation memory.


Live: stars.realm.watch · Operator's runbook: RUNBOOK.md · Design brief: docs/2026-05-02-starcharts-design-brief.md


✦ The idea

A small group — a household, a couple, a friend cluster — opens a chart. A chart is a goal-reward pact:

50 stars → movie night. 20 stars → a quiet weekend away. 3 stars → ice cream.

Members give each other stars when they notice each other doing something worth noticing. Each gift is a small constellation — same artwork, scattered softly through the chart's sky. New stars land; they never pop in. Older stars drift to softer constellations behind. The sky tells the truth of the chart at a glance.

When the goal is met, the sky bursts, the reward rises in serif, and the chart becomes a memory — not archived, just hung gently on the wall.

A 7-year-old should find it magical. An adult should find it tasteful. The whole thing should feel handcrafted, never template-y.


✦ Two ways to summon a star

Preset stars — a curated gallery of fourteen art-directed star families: gold sparkle, ruby twinkle, amethyst nebula, silver crescent, aurora ribbon, pearl shimmer, comet trail, supernova bloom, emerald glint, copper ember, frost crystal, cosmic rose, rainbow burst, dragon fire. Distinctive at a glance, even at thumbnail size. Free to give, abundant by design. (See app/src/lib/presets.ts for the canonical order; assets live under assets/stars/ and ship as app/public/stars/<slug>.png.)

Custom stars — type a single line of poetry like "a star made of fireflies and starlight", press Summon, and the app calls Azure AI Foundry's gpt-image-1.5 to mint a one-of-a-kind transparent PNG just for that gift. Cosmic dust gathers while it forms. The arrival feels earned. (Custom mode costs real money per generation, so it's framed as a small ceremony, not a casual tap.)


✦ What's in this repo

starcharts/
├── app/                ← React + Vite SPA (the sky, the gifts, the memories)
│   ├── src/
│   │   ├── screens/    ← SignIn, Dashboard, ChartSky, GiftFlow, SummonFlow,
│   │   │                 GoalReached, ConstellationMemory, …
│   │   ├── components/ ← Sky, Star, GiftCard, PresetGallery, LoadingSky,
│   │   │                 Sigil (visible version badge), …
│   │   ├── design/     ← tokens, theme, globals.css (light + dark)
│   │   ├── db/         ← InstantDB client + schema bindings
│   │   └── hooks/      ← useChart, useGiftsForChart, presence, …
│   ├── public/         ← favicon.svg, og.png (1200×630 social card),
│   │                     CNAME, version.json (written at build)
│   └── scripts/        ← version.mjs (writes /version.json — runs as
│                         predev + prebuild so it's present in dev too)
│
├── worker/             ← Cloudflare Worker `starcharts-summon`
│   └── src/index.ts    ← POST /api/summon → Azure → R2 → { url }
│                         (KV-backed rate-limits, optional binding)
│
├── assets/stars/       ← preset star artwork (PNG/SVG)
├── design_handoff/     ← fidelity references from the design brief
├── docs/               ← design brief, port plan, preset previews,
│                         email/magic-code.html (mirror of the
│                         InstantDB magic-link template)
├── scripts/            ← generate-stars.py (Python tooling for preset
│                         assets), og-source.html (OG card source)
├── .github/workflows/  ← deploy.yml — GH Pages on push to main
└── RUNBOOK.md          ← operations: deploy, smoke, rotate, troubleshoot

✦ Architecture, in one breath

                      ┌──────────────────────────┐
                      │   stars.realm.watch      │  GitHub Pages
                      │   (React/Vite SPA)       │  static, HashRouter
                      └────────────┬─────────────┘
                                   │
          real-time sync ◄─────────┼─────────► magic-link auth
                                   │
                          ┌────────▼────────┐
                          │    InstantDB    │   users · groups · charts · gifts
                          └─────────────────┘
                                   │
        custom-star summon ────────┼──────► POST /api/summon
                                   ▼
                  ┌───────────────────────────────┐
                  │ summon.stars.realm.watch      │  Cloudflare Worker
                  │ (starcharts-summon)           │
                  └───────┬──────────────┬────────┘
                          │              │
                          ▼              ▼
              Azure AI Foundry        Cloudflare R2
              gpt-image-1.5           starcharts-customs
              (transparent PNG)       (durable storage)
  • Frontend: React 19, Vite 8, React Router 7 (HashRouter), Framer Motion, React Spring, Lottie, InstantDB React client.
  • State + sync + auth: InstantDB (single-tenant, app id baked in app/src/db/client.ts).
  • Image gen proxy: one Cloudflare Worker. Validates the prompt, calls Azure, stores the PNG in R2, returns a public URL. Never exposes the API key to the browser. Rate-limited per group/day and per-IP/hour via a Cloudflare KV namespace — see RUNBOOK → Summon rate-limits.
  • Invite-code lookup: the same Worker exposes POST /api/join-group, which resolves an invite code to a groupId using the InstantDB admin token. This lets groups.view stay locked to members only — enumeration of the 32⁶ ≈ 1B keyspace is rate-limited per IP at the edge. See worker/README.md for endpoint shape and bucket sizes.
  • Versioning: app/scripts/version.mjs writes /version.json at build with git metadata (hash, branch, dirty, built). It runs as both predev and prebuild so the file is present in npm run dev too. Realm word is stellar, per the project-wide realm-sigil convention. A small <Sigil /> badge in the bottom-left corner reads that file at runtime and shows the live build at a glance — pulsing ✦ + realm word
    • short hash, hover-expand into branch + commit + GitHub-link panel.
  • Social previews: static og:* / twitter:* tags in app/index.html, paired with app/public/og.png (1200×630). HashRouter means scrapers only see the index <head>, so one card serves every shared link.

✦ Quickstart

You'll need Node 20+, npm, and — for the custom-star path — an Azure AI Foundry deployment of gpt-image-1.5.

1. Run the SPA locally

cd app
npm install
npm run dev          # → http://localhost:5173

The app talks to a real InstantDB project out of the box, so sign-in works locally on the first run. Magic-link emails arrive at whatever address you type.

2. Run the summoning Worker locally (optional)

Custom stars work in production by default. To exercise the path locally:

cd worker
npm install
cp .dev.vars.example .dev.vars   # then fill in real Azure secrets
npm run dev                       # → http://localhost:8787

Then point the SPA at it:

# app/.env.local
VITE_SUMMON_ENDPOINT=http://localhost:8787/api/summon

The dev server hot-reloads. Open a chart, hit Summon in /charts/:id/give, and watch worker/src/index.ts log the round-trip.


✦ The ten scenes

These are the moments the design must nail. Each lives in its own file — follow the trail if you want to see how a feeling got built.

Scene What it feels like Where it lives
Sign-in a single drifting star, a soft welcome app/src/screens/SignIn.tsx
Profile setup choose your name in the sky app/src/screens/ProfileSetup.tsx
Group setup open a group or step into one with an invite code app/src/screens/GroupSetup.tsx
Dashboard each chart is a tiny living sky app/src/screens/Dashboard.tsx
Create chart three fields, ceremonious, then a sky opens app/src/screens/CreateChart.tsx
Inside a chart the sky is the page; presence is felt, not announced app/src/screens/ChartSky.tsx
Give a gift honoree → reason → preset (or summon) → count → send app/src/screens/GiftFlow.tsx
Summon a custom star type, breathe, dust gathers, a star arrives app/src/screens/SummonFlow.tsx
Goal reached coordinated burst, reward rises in serif app/src/screens/GoalReached.tsx
Constellation memory the chart, kept app/src/screens/ConstellationMemory.tsx

✦ Data shape

Four entities, five named links — relationships are modeled as InstantDB links rather than foreign-key columns, so queries traverse them with dotted paths like where: { "group.id": groupId }. See app/src/db/schema.ts for the source of truth.

$users         { id, email, displayName, avatarSeed }
groups         { id, name, inviteCode, createdAt }
charts         { id, name, goalCount, reward,
                 createdAt, completedAt? }
gifts          { id, reason, count, style,
                 starImageUrl, x, y, createdAt }

groupMembers   ─── link ─── groups.members  ↔ $users.groups
chartGroup     ─── link ─── charts.group    ↔ groups.charts
giftChart      ─── link ─── gifts.chart     ↔ charts.gifts
giftGiver      ─── link ─── gifts.giver     ↔ $users.given
giftHonorees   ─── link ─── gifts.honorees  ↔ $users.received

A gift of count: N renders as a cluster of N stars: same artwork, hand-feeling positions. The (x, y) on a gift is the cluster's anchor point in normalized 0–1 sky coordinates; the per-star offsets are derived from a stable seed at render time. starImageUrl is the resolved sprite source — a /stars/<slug>.png for preset gifts, or an R2 URL minted by the Worker for custom-style gifts (style === "custom"). Cluster variety comes from spatial arrangement, never from per-star variance.

A chart's completedAt flips when the sum of its gifts' count values crosses goalCount. From that moment, every member's client routes to /charts/:id/celebrate, and afterwards to /charts/:id/memory. Past charts are never deleted — they keep their shape forever.

The chart sky is pannable (drag empty space) and gift clusters are draggable (press-hold a star and drag) so members can arrange the sky intentionally. The giver of a gift can also remove their own stars from /charts/:id via the Remove gift affordance in the gift card; once a chart is sealed into memory, removal is no longer surfaced (the memory view stays read-only by design).


✦ Permissions, in one breath

InstantDB permission rules live at app/src/instant.perms.ts and are pushed to production via npx instant-cli push perms -a <APP_ID> from app/. Summary:

  • $users — visible to self + group-mates only.
  • groups — view / rename: members only. Self-join: a non-member can link themselves into members via the invite-code flow. inviteCode and createdAt are immutable after creation.
  • charts — view: members of the chart's group. Update: only completedAt, one-way null → timestamp. Everything else pinned at creation.
  • gifts — view / create: members of the chart's group. Update: x/y only (cluster repositioning). Delete: giver-only.

Group enumeration is impractical from the public app: groups.view is locked to members, and the only way to discover a group id from outside is through the rate-limited /api/join-group Worker endpoint.


✦ Deploy

A push to main triggers .github/workflows/deploy.yml, which builds the SPA and publishes it to GitHub Pages at stars.realm.watch. The Worker is deployed independently:

cd worker
npm run deploy        # wrangler deploy → starcharts-summon
npm run tail          # stream live logs

Full operational detail — first-time DNS, R2 setup, secret rotation, smoke procedure, every known failure mode and its signature — lives in RUNBOOK.md.


✦ Quality bar

The non-negotiables, copied forward from the design brief:

  • The sky is alive — subtle drift, slow twinkle, parallax depth. Never jittery, never stiff.
  • New stars arrive. They do not pop in. The landing animation is the small joyful moment that makes the app addictive.
  • Cluster stars look related (close in space, identical artwork) without looking mechanical (positions feel hand-placed, never gridded).
  • Real-time presence is a feature, not a side-effect — when another member is in the same chart, you can feel them there.
  • Empty states are beautiful. A fresh sky should feel hopeful, not bare.
  • Tap targets are generous (kids' fingers).
  • Microcopy is gentle and warm, never sterile.
  • Both light and dark are intentional designs, not auto-derived recolors.
  • prefers-reduced-motion is honored — animations resolve to instant arrivals.

✦ Known limits (v1)

A handful of intentional shortcuts are listed in RUNBOOK.md → Known limitations: unindexed createdAt, single-tenant InstantDB app id, best-effort presence, mostly-immutable gifts (only x/y editable, only by group members; only the giver can remove), magic-link mail still via InstantDB's locked template (only the app name + sender display-name are brandable from their dashboard). Each one has a documented migration path for when scale or polish demands it.

Custom-star summons are rate-limited at the Cloudflare Worker — 10/group/day, 30/IP/hour — to keep Azure spend bounded. The /api/join-group lookup is rate-limited at 10/IP/hour so brute-force enumeration of the invite-code keyspace stays impractical. See RUNBOOK → Summon rate-limits for the constants and how to bump them.

Tracked open issues: #18 (custom domain TLS handshake — workers.dev fallback in place), #9 (per-board collaborators — architecture exploration).


✦ License

Private project. Ask JP (jp@jphein.com) before mirroring or redistributing.


                       ✦ travel under a generous sky ✦

About

A collaborative starchart for families, couples, and friend groups — give each other stars for meaningful acts, earn real-world rewards together.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors