experimental bluesky client
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

TypeScript 88.1%
CSS 11.9%
13 1 0

Clone this repository

https://tangled.org/dude.computer/dudesky https://tangled.org/did:plc:lzlgtwvovfayiwdv2yootme3/dudesky
git@tangled.org:dude.computer/dudesky git@tangled.org:did:plc:lzlgtwvovfayiwdv2yootme3/dudesky

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

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_state and oauth_session tables)
  • 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 — not Response.redirect() (immutable headers) and not TanStack Router's redirect() 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.