This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Run all tests deno test --allow-import --allow-env # Run specific test file deno test --allow-import --allow-env tests/sync-service.test.ts deno test --allow-import --allow-env tests/post-transformer.test.ts deno test --allow-import --allow-env tests/storage.test.ts # Run single test case deno test --allow-import --allow-env tests/sync-service.test.ts --filter "should sync posts successfully"
# Lint all files deno lint # Format code deno fmt # Type check deno check backend/index.ts deno check cronjob.ts
- Set
backend/index.ts
as HTTP val (serves web interface and API) - Set
cronjob.ts
as Cron val with schedule*/15 * * * *
(every 15 minutes) - You can
vt push
to push changes to valtown so you can test the updated online endpoints
- Set
VALTOWN_URL
environment variable to your Val.town URL (e.g.,https://your-username--unique-id.web.val.run
) - Set
ATPROTO_APP_PASSWORD
environment variable to your Bluesky App Password for sync service - Set
ATPROTO_ALLOWED_HANDLE
environment variable to your Bluesky handle (e.g.,username.bsky.social
) to restrict OAuth setup to your account only - The client metadata is automatically generated at
/client
endpoint
- Go to Bluesky Settings → Privacy and Security → App Passwords
- Click "Add App Password"
- Give it a name like "ATProto to Fediverse Sync"
- Copy the generated password and set it as
ATPROTO_APP_PASSWORD
environment variable - The sync service will automatically use App Password authentication when available
This is a single-user service. To prevent unauthorized users from hijacking your bridge:
- Set
ATPROTO_ALLOWED_HANDLE
to your Bluesky handle (e.g.,tijs.org
orusername.bsky.social
) - Keep your setup URL private - anyone with the URL can attempt OAuth, but only your handle will be accepted
- OAuth verification: The service will reject OAuth attempts from any
handle that doesn't match
ATPROTO_ALLOWED_HANDLE
Example rejection message:
"This service is configured for tijs.org only. You are logged in as someone.else.bsky.social."
- Make it a point to run deno lint, test and fmt after any big change
- You can use curl to check endpoints yourself if you are debugging
This is a single-user bridge service that cross-posts from Bluesky to Mastodon, built specifically for Val.town with dependency injection for testability.
The codebase follows a dependency injection pattern with clear separation between:
- Interfaces (
backend/interfaces/
) - Abstract contracts for storage and HTTP clients - Implementations (
backend/storage/
,backend/services/
) - Concrete implementations - Tests (
tests/
) - Use in-memory mocks for fast, isolated testing
Storage Layer: Uses abstract StorageProvider
interface with SQLite
production implementation and in-memory test implementation. Single-user
architecture - all database tables enforce single-row constraints with
CHECK (id = 1)
.
HTTP Client Layer: ATProto and Mastodon interactions are abstracted through interfaces, allowing mock implementations for testing.
Service Layer: SyncService
(dependency injection version) accepts
dependencies via constructor injection, making it fully testable without
external dependencies. No userId parameters - all methods work with the
single user.
OAuth Flow: Two separate OAuth implementations (ATProto uses PKCE + DPoP,
Mastodon uses traditional OAuth2) handled in backend/routes/oauth.ts
.
Session Management: SQLite-backed sessions for Val.town persistence (serverless environment doesn't persist in-memory data).
- Setup: User runs setup wizard → OAuth tokens stored in SQLite (single user account)
- Sync: Cron job (
cronjob.ts
) →SyncService.syncUser()
→ fetches posts from ATProto → transforms content → posts to Mastodon - Transformation:
PostTransformer
converts Bluesky mentions to profile links since Mastodon handles don't exist cross-platform - Tracking: Every post sync is tracked in database with status (pending/success/failed) and retry logic
backend/index.ts
- Main HTTP server (Hono app) serving setup wizard and OAuth callbackscronjob.ts
- Scheduled sync job (runs every 15 minutes)backend/services/sync-service-di.ts
- Main sync logic with dependency injection (single-user)backend/services/sync-service.ts
- Alternative sync service (simplified, single-user)backend/interfaces/
- Abstract contracts for testability (single-user interfaces)backend/storage/
- SQLite (production) and in-memory (testing) implementationsbackend/database/
- Database schema and queries (single-user constraints)backend/lib/
- Session management and debug loggingfrontend/
- Multiple HTML pages and React components (landing, setup, dashboard)shared/types.ts
- TypeScript interfaces shared between frontend and backend
Tests use in-memory storage and mock HTTP clients for fast, isolated testing:
InMemoryStorageProvider
- Full single-user storage implementation in memoryMockATProtoClient
/MockMastodonClient
- Controllable mock API clients- Tests verify business logic without external dependencies
- All tests updated for single-user architecture (no userId parameters)
Built for Val.town's serverless environment:
- Uses Val.town's SQLite hosting (
https://esm.town/v/stevekrouse/sqlite
) - Uses Val.town utility functions (
https://esm.town/v/std/utils
) - Follows Val.town's file serving patterns for static assets
- Environment variables for OAuth configuration
- SQLite-backed sessions for persistence across serverless requests
- Single-user architecture perfect for personal Val.town deployments
ATProto OAuth: Uses PKCE flow with DPoP (Demonstration of Proof of
Possession) for enhanced security. Client metadata is automatically served at
/client
endpoint.
Mastodon OAuth: Automatically registers app with user's Mastodon instance during setup flow.
Session Management: Cookie-based sessions stored in SQLite for persistence in serverless environment.
The PostTransformer
handles the key business logic:
- Converts
@handle.bsky.social
mentions tohttps://bsky.app/profile/handle.bsky.social
links - Extracts and processes media (images/videos)
- Generates content hashes for duplicate prevention
- Handles ATProto facets (mentions, hashtags, links)
Implements exponential backoff retry mechanism:
- 3 retry attempts by default
- Base delay: 1 second, max delay: 30 seconds
- Distinguishes between retryable (network, 5xx) and non-retryable errors
- All errors logged to database for debugging
- Debug logging system accessible via browser (
/api/debug/logs
) since Val.town doesn't provide server logs
Key principle: All tables enforce single-row constraints with
CHECK (id = 1)
:
bridge_user_accounts_v1
- Single user account (no user_id field)bridge_settings_v1
- Single user settingsbridge_post_tracking_v1
- Post sync tracking (no user_id field)bridge_sync_logs_v1
- Sync operation logs (no user_id field)sessions
- Cookie-based sessions
Single-user methods (no userId parameters):
getUserAccount()
/getSingle()
- Get the single userupdateUserAccount(updates)
/updateSingle(updates)
- Update single usergetSettings()
/getSingle()
- Get single user settingspostTracking.getByUri(uri)
- Get post by URI (no userId)postTracking.updateByUri(uri, updates)
- Update post by URI