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
backend/index.ts
as HTTP val (serves web interface and API)cronjob.ts
as Cron val with schedule */15 * * * *
(every 15 minutes)vt push
to push changes to valtown so you can test the updated
online endpointsVALTOWN_URL
environment variable to your Val.town URL (e.g.,
https://your-username--unique-id.web.val.run
)ATPROTO_APP_PASSWORD
environment variable to your Bluesky App Password
for sync serviceATPROTO_ALLOWED_HANDLE
environment variable to your Bluesky handle
(e.g., username.bsky.social
) to restrict OAuth setup to your account only/client
endpointATPROTO_APP_PASSWORD
environment
variableThis is a single-user service. To prevent unauthorized users from hijacking your bridge:
ATPROTO_ALLOWED_HANDLE
to your Bluesky handle (e.g., tijs.org
or
username.bsky.social
)ATPROTO_ALLOWED_HANDLE
Example rejection message:
"This service is configured for tijs.org only. You are logged in as someone.else.bsky.social."
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:
backend/interfaces/
) - Abstract contracts for storage and
HTTP clientsbackend/storage/
, backend/services/
) - Concrete
implementationstests/
) - Use in-memory mocks for fast, isolated testingStorage 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).
cronjob.ts
) → SyncService.syncUser()
→ fetches posts
from ATProto → transforms content → posts to MastodonPostTransformer
converts Bluesky mentions to profile
links since Mastodon handles don't exist cross-platformbackend/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 backendTests use in-memory storage and mock HTTP clients for fast, isolated testing:
InMemoryStorageProvider
- Full single-user storage implementation in memoryMockATProtoClient
/ MockMastodonClient
- Controllable mock API clientsBuilt for Val.town's serverless environment:
https://esm.town/v/stevekrouse/sqlite
)https://esm.town/v/std/utils
)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:
@handle.bsky.social
mentions to
https://bsky.app/profile/handle.bsky.social
linksImplements exponential backoff retry mechanism:
/api/debug/logs
) since Val.town
doesn't provide server logsKey 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 sessionsSingle-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