atmo.quest — Product Spec #
Working document for the atmo.quest project. Intended audience: human collaborators and the AI assistants helping them. If you are an AI assistant reading this as context, treat decisions in this doc as authoritative unless the user explicitly overrides them in conversation.
Status: Pre-launch, targeting CascadiaJS 2026 (June 1–2, Seattle) Domain: atmo.quest Last updated: May 2026
What it is #
A conference companion app on ATProto. You scan a QR, you take a note, you leave with a real follow-up list. Your connections live in your own ATProto repo, not in a database that will be gone next year.
The launch event is CascadiaJS 2026. The longer-term goal is a general-purpose tool any organizer can use for their event.
One-line pitch #
The people you meet are yours to keep.
Why it exists #
Conferences produce stacks of business cards and zero memory of who anyone was. Closed event apps own that data and shut down. ATProto offers a path where connections, notes, and event records live in the attendee's own repo — portable, durable, theirs.
Goals #
- Make it easy to connect at events through ATProto. Scanning a QR should work in under 5 seconds, with or without an existing account.
- Remove the "you need an account first" wall. Guest mode collects connections in local storage; account creation happens on the user's schedule, not the app's.
- Demonstrate ATProto's portability story in a concrete way. Records are written to user PDSes. The lesson is in the product, not the marketing copy.
- Ship something polished for CascadiaJS. A working, polished v1 by June 1, 2026.
Non-goals (v1) #
- Replacing Bluesky, Linkedin, or any general-purpose social app
- Real-time messaging between users
- Public profiles browsable outside an active event
- Multi-event analytics dashboards for organizers
- The "side quests published by other atproto accounts" idea from Will — interesting but explicit scope creep for v1
Aesthetic direction #
Terminal Quest. Catppuccin-themed terminal/dev aesthetic. JetBrains Mono for body, Instrument Serif italic for display accents. Peach (#fab387) as the accent color, lavender (#b4befe) for secondary highlights. Dark theme only for v1.
Reference files: app-screens.html, design-3-terminal.html in the design folder.
Avoid: generic SaaS landing-page aesthetics, purple gradients on white, Inter/Roboto/system fonts. The audience is JS developers at Cascadia; they recognize and reject generic AI design.
Users #
- The connected attendee — already has a Bluesky/ATProto account. Most ATProto-curious folks.
- The guest attendee — does not have a Bluesky/ATProto account. Wants to scan a stranger's QR without committing to anything first.
- The event organizer (post-Cascadia) — wants to set up an event for their meetup/conf.
- The admin — Brittany + core team. Needs usage stats and operational visibility.
Features #
Onboarding #
When a user lands on atmo.quest cold #
- See the home page (
design-3-terminal.html) - Two CTAs: "Start scanning" and "What's ATProto?"
- "What's ATProto?" routes to
/about-atproto
When a user scans someone's QR before signing in #
- Land on the scanned person's profile preview
- Prompt: "Connect with [name]? You don't need an account yet."
- If accepted: connection saved to local storage, prompted lightly to sign up "when you have time"
- If user later signs up, local connections are migrated to their PDS
Sign-up flow #
- "Do you already have an Atmosphere account?" → Login (OAuth via
@atproto/oauth-client-*) - "Create an account" → default flow creates account on
bsky.social - Advanced (disclosure): "Choose your own PDS / handle" — hidden behind a collapse for v1 to avoid edge-case support load
- Stretch (not v1): "Log in with Mastodon via bridgy fed"
Account creation default #
- New accounts default to
bsky.socialPDS - Default handle suggestion: from user's name, like
firstname.bsky.social - The user can override but the defaults make signup near-frictionless
Profile #
Self profile #
- Pull display name, avatar, bio from the user's
app.bsky.actor.profile - Enrich with atmo.quest-specific fields:
- Important links (GitHub, personal site, mastodon, etc. — arbitrary pills)
- Interests (free-text tags, used for matching at events)
- Location (city/region)
- Where they work
- Best contact method
- Optional flags:
hiring(employer),looking(job seeker)
- These extra fields live in a
quest.atmo.profilerecord in the user's repo
Other person's profile (after connecting) #
- All public profile info
- "Connected · [date]" badge
- Hiring/looking badges if set
- Pill links and interests
- Notes section (private to the viewer, local storage only for v1)
- Follow-up checkbox — flags this person for the post-conference summary
QR / Connection #
- Each logged-in user has a QR code that encodes a
bsky://orat://URI pointing to their profile - Home screen shows your avatar; tap image to show QR code to QR
- Scanning a QR:
- Both users logged in: writes a
quest.atmo.connectionrecord to each user's repo - Scanner is a guest: saves the connection to local storage with a "claim me later" flag
- Scanned person is a guest: rare edge case; same as above but reversed
- Both users logged in: writes a
- Connections during an active event window (both parties checked in, scan happens within event start/end) are automatically associated with that event in the connection record
- Users can connect to the same person at multiple future events; each creates a new connection record with a different event reference (or no event)
Stretch: mobile app with widget #
- Native iOS/Android wrapper around the web app
- Home screen widget showing your QR code for one-tap-from-lockscreen sharing
- Not in v1 scope, but treat as plausible v1.1
Events #
Event creation #
- Hidden from public UI until after CascadiaJS — "hey, you can use this at your events too!" launch
- Required fields: name, start time, end time, expected participant count
- Optional fields: geofence (lat/long + radius), location name, organizer Bluesky handle
- Calculated: total possible unique connecting pairs = N attendees (NOT N×(N−1)/2, see Metrics section)
Event check-in #
- Triggered manually by the user (tap "check in" on the event)
- Optional soft geofence check: if user has location permission and is outside the radius, show "you don't seem to be at the event, are you sure?" prompt — never a hard block (indoor venues mess with GPS, desktop users have no GPS)
- Writes a
quest.atmo.checkinrecord to the user's repo, referencing the event
Event merging (post-Cascadia) #
- Two event creators can merge their events into a single super-event
- Both get the original event-starter badge plus a special "interop" badge
- Connection records continue to reference the merged event
Unlocks (group progress) #
Progress metric: unique attendees who have made at least one connection at the event. NOT total connection count (gameable), NOT N×(N−1)/2 possible pairs (mathematically unreachable for any real event size).
Unlock thresholds for CascadiaJS (375-attendee estimate, ~30% adoption target = ~115 active users):
| Threshold | Unlock | Notes |
|---|---|---|
| 1 (everyone, on check-in) | Badge, mini profile, scan-to-connect, private notes, basic post-conference summary | Default |
| 20 unique connectors | Leaderboard appears | Early signal, ~6% adoption |
| 75 unique connectors | Interest matching surface (see attendees with overlapping interests you haven't connected with) | Mid, ~20% adoption |
| 150 unique connectors | Enhanced in-app summary with stats | Stretch, ~40% adoption |
Note: These numbers should be revisited closer to the event based on actual ticket sales and known attendance. Numbers must feel reachable but not trivial.
The progress bar should fill via WebSocket so the experience feels alive when more people are joining.
Badges #
Stored as quest.atmo.badge records in user repo. Cosmetic, not gating.
first-connect— your first connection evertotal-connections-50,-100,-200, etc.profile-complete— all profile fields filledevent-attendee— checked in to an eventevent-starter— created an eventevent-interop— merged your event with anotherevent-unlock-N— earned per group-unlock tier hit (e.g. you were here when the leaderboard unlocked)
Notes & Follow-ups #
- Notes are local storage only for v1. Reasoning: ATProto doesn't have production-ready private records yet (see Daniel Holmgren's permissioned data work). Putting personal observations about someone in your public repo is bad. Notes live in
localStoragekeyed by(viewer DID, target DID). - Follow-up flag on each connection — included in the post-conference summary email/export.
- Future migration plan: when ATProto supports private records, offer to migrate notes to the user's repo.
Post-conference summary #
- Everyone gets a basic summary: list of people met, your notes, links to their profiles
- Enhanced version (at 150-unlock): adds interest overlap analysis, "people you almost met" (shared interests, didn't connect), conference-wide stats
- Delivered: in-app view + optional emailed/exported markdown
About-ATProto page #
/about-atproto- Plain-language explanation of:
- What ATProto is (decentralized social protocol, your data lives in your repo)
- What a PDS is, what a handle is
- Why atmo.quest is built on it
- How to get involved (links to Bluesky, atproto.com, other atmosphere apps)
- Link visible from onboarding ("What's ATProto?" CTA) and from a header link on the home page
Admin interface #
- Auth: any logged-in user with
is_admin = truein the database (set initially via DB script) - Pages:
- Usage summary (DAU, total connections, total users, active events)
- User list (search by handle)
- Event list with per-event stats
- Connection trends over time
- Initial admins: Brittany + core team
Lexicons (preliminary) #
These are working drafts. Real lexicon definitions to be finalized before the alpha. Namespace
quest.atmo.*(reverse-DNS of theatmo.questdomain).
quest.atmo.profile #
{
"links": [{ "label": "GitHub", "url": "..." }, ...],
"interests": ["edge computing", "rust", ...],
"location": "Vancouver, WA",
"worksAt": "GitHub",
"contactMethod": "DM on bsky",
"hiring": false,
"looking": true,
"updatedAt": "2026-05-..."
}
quest.atmo.event #
{
"name": "CascadiaJS 2026",
"startTime": "2026-06-01T08:00-08:00",
"endTime": "2026-06-02T23:00-08:00",
"expectedAttendees": 375,
"location": "Seattle, WA",
"geofence": { "lat": ..., "lng": ..., "radiusMeters": 500 },
"organizerDid": "did:plc:..."
}
quest.atmo.checkin #
{
"event": "at://did:plc:.../quest.atmo.event/cascadiajs-2026",
"checkedInAt": "2026-06-01T09:42-08:00"
}
quest.atmo.connection #
{
"with": "did:plc:...",
"connectedAt": "2026-06-01T10:42-08:00",
"event": "at://did:plc:.../quest.atmo.event/cascadiajs-2026" // optional
}
quest.atmo.badge #
{
"type": "first-connect" | "event-attendee" | ...,
"earnedAt": "2026-06-01T10:42-08:00",
"event": "at://..." // optional, for event-specific badges
}
Screens #
Reference: app-screens.html
Home #
- Top bar: logo + user handle
- Greeting + time
- QR flip card (tap avatar to flip to QR)
- Active event banner (when checked into one) — links to event screen
- Recent connections grid (4-col, ~8 visible, tap to view profile)
Connections (replaces "last seen" tab) #
- Full list of all your connections (across all events + non-event)
- Search by handle
- Filter by event (dropdown)
- Sort: most recent, alphabetical, has-follow-up-flag
- Tap any connection → that person's profile screen
Profile (other person) #
- Avatar + name + handle
- Connection metadata badge ("connected · Jun 1")
- Hiring/looking badges
- Profile detail card (bio, works at, location, contact)
- Pill rows: links + interests
- Notes textarea (private, local)
- Follow-up checkbox
Event #
- Event title, dates, status indicator (live / upcoming / ended)
- Progress bar with tick-marks for unlock thresholds
- Earned/locked rewards list
- Checked-in attendees grid
- Connected = color avatar w/ peach border
- Unconnected = grayscale avatar
- Filter: all / mine
- Live (WebSocket) indicator
Bottom nav #
- Home / Connections / Event (when applicable, otherwise Events list)
Tech stack (proposed) #
These are starting points, not commitments. Adjust per implementation reality.
- Frontend: TBD — needs to be lightweight, mobile-first, dark-mode-only, offline-capable for the guest-mode local-storage path. Astro + a sprinkle of interactivity, or a plain React SPA, both viable.
- Auth:
@atproto/oauth-client-browserfor browser-based OAuth,@atproto/oauth-client-nodefor any server-side flows - Records: Standard
@atproto/apiAgentfor record CRUD - Backend: Minimal — needs to handle the WebSocket for live event progress, store admin flags, possibly cache event stats. Could be a thin Node/Hono server.
- Database: Whatever's quickest. SQLite + Kysely (like Statusphere) is fine for v1.
Marketing #
- May 31 pre-party: hacker train to Seattle, socialize the app in person
- Printed postcards with QR to atmo.quest and a "best way to connect at CascadiaJS" tagline
- ATProto PDX community sponsor slot: mention during their stage time
- Brittany's talk is near the end of the conference — final shoutout, possibly a live stats reveal if unlocks have happened
- Emcee/organizer shoutouts where possible
- Bluesky posts from
@brittany.devand atmosphere community accounts
Open questions / decisions pending #
Hard rules for AI assistants helping on this repo #
- Notes are local storage only in v1. Do not write code that puts user notes about other people into a public PDS record.
- Do not invent ATProto API behavior. Check
@atproto/apidocs, atproto.com, or the bluesky-social/atproto repo for actual specs. - Default new accounts to
bsky.social. Custom PDS / handle is an advanced disclosure, not a top-level choice. - The aesthetic is Terminal Quest (Catppuccin + JetBrains Mono + Instrument Serif italic accents, peach as accent). Do not propose Inter, do not propose purple gradients on white.
- The unlock metric is unique attendees who have made at least one connection. Not total connections, not pairs.
- Geofencing is a soft prompt, never a hard block.