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.tsas HTTP val (serves web interface and API) - Set
cronjob.tsas Cron val with schedule*/15 * * * *(every 15 minutes) - You can
vt pushto push changes to valtown so you can test the updated online endpoints
- Set
VALTOWN_URLenvironment variable to your Val.town URL (e.g.,https://your-username--unique-id.web.val.run) - Set
ATPROTO_APP_PASSWORDenvironment variable to your Bluesky App Password for sync service - Set
ATPROTO_ALLOWED_HANDLEenvironment 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
/clientendpoint
- 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_PASSWORDenvironment 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_HANDLEto your Bluesky handle (e.g.,tijs.orgorusername.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:
PostTransformerconverts 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.socialmentions tohttps://bsky.app/profile/handle.bsky.sociallinks - 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