Dudesky#
An experimental Bluesky client built to explore ATProto, TanStack, and modern frontend web development.
What it does#
- Sign in with your Bluesky handle via ATProto OAuth (DPoP + PKCE)
- Browse your home feed with reply-parent context
- Navigate full conversation threads (ancestors + replies)
- Image grids and link card previews for embedded content
Stack#
| Layer | Tech |
|---|---|
| Framework | TanStack Start (SSR, file-based routing, server functions) |
| Routing | TanStack Router |
| UI | React 19, Tailwind CSS v4 |
| ATProto | @atproto/api, @atproto/oauth-client-node |
| Persistence | better-sqlite3 (OAuth state + session store) |
| Linting/Formatting | Biome |
| Testing | Vitest + Playwright |
Getting started#
npm install
Create a .env file with the required environment variables:
VITE_APP_URL=http://localhost:3000
# DPoP key pair (generate with the ATProto key tool or openssl)
PRIVATE_KEY_0=...
PRIVATE_KEY_1=...
PRIVATE_KEY_2=...
# Optional: override the SQLite DB path (defaults to dudesky.db in the project root)
# DB_PATH=/path/to/dudesky.db
Then start the dev server:
npm run dev
The app runs on port 3000 by default. Open it, enter your Bluesky handle, and sign in via the ATProto OAuth flow.
Project structure#
src/
components/ # Shared UI components (Header, Footer, EmbedBlock)
lib/ # Server-only utilities (DB setup, OAuth client, types)
routes/ # File-based routes (TanStack Router)
__root.tsx # App shell, root loader for auth state
index.tsx # Landing page
login.tsx # Handle entry + OAuth redirect
callback.tsx # OAuth callback, sets DID cookie
feed.tsx # Home timeline
post.$uri.tsx # Thread view (ancestors + focal post + replies)
logout.tsx # POST handler, clears session
client-metadata.tsx # ATProto OAuth client metadata endpoint
jwks.tsx # JWK set endpoint
styles.css # Tailwind v4 + custom design system
Design system#
The app has a custom beach/ocean-themed design system defined in src/styles.css:
- Fonts: Manrope (body), Fraunces (display headings)
- Color tokens:
--sea-ink,--lagoon,--palm,--sand,--foam, and friends — all with light/dark variants - Utility classes:
.island-shell(frosted-glass cards),.island-kicker(small-caps labels),.display-title,.rise-in(entrance animation),.nav-link
Tokens can be used as Tailwind values directly: text-[--sea-ink-soft], bg-[--surface], border-[--line].
Scripts#
npm run dev # Start dev server (port 3000)
npm run build # Production build
npm run preview # Preview production build
npm run test # Run Vitest unit tests
ATProto notes#
- OAuth state and session tokens are persisted in SQLite (
oauth_stateandoauth_sessiontables) - The authenticated DID is stored in a plain (non-encoded) cookie — colons in DIDs are valid in cookie values and must not be percent-encoded or
client.restore()will fail - Server route handlers use
new Response(null, { status: 302, headers: {...} })for redirects — notResponse.redirect()(immutable headers) and not TanStack Router'sredirect()helper
Purpose#
This is a learning project — not a production client. The goal is to understand ATProto's OAuth and data model, get hands-on with TanStack Start's SSR + server functions model, and experiment with modern frontend patterns.