Mulberry is a paired-device Android app for two people who share a persistent drawing canvas, with a Next.js landing site in the same Turborepo workspace. The foreground app is where users draw, pair, and manage settings; your wallpaper shows the latest local canvas snapshot through an Android live wallpaper.
The product is built for ambient connection, not group collaboration. Active devices synchronize quickly over WebSocket, while sleeping or backgrounded devices converge later through push-triggered recovery and revision reconciliation.
- Features
- Architecture
- Repository layout
- Prerequisites
- Getting started
- Configuration
- Production deployment
- Development commands
- API overview
- Testing
- Contributing
- Security
- License
- Troubleshooting
- Pair exactly two Android users into a private shared session.
- Authenticate with Google and maintain local session bootstrap state.
- Generate and redeem short invite codes for pairing.
- Draw freehand strokes with color and width controls.
- Synchronize canvas operations with server-assigned revisions.
- Render the shared canvas locally for immediate feedback.
- Display the latest cached canvas state through a live wallpaper.
- Recover missed operations after process death, network loss, or idle periods.
- Register Firebase Cloud Messaging tokens for background canvas catch-up.
flowchart LR
A["Android app<br>Device A"] -->|REST + WebSocket ops| B["Fastify backend"]
C["Android app<br>Device B"] -->|REST + WebSocket ops| B
B --> D["PostgreSQL<br>users, sessions, invites, ops, snapshots"]
B --> E["Firebase Cloud Messaging"]
E --> A
E --> C
A --> F["Live wallpaper<br>local cached render"]
C --> G["Live wallpaper<br>local cached render"]
The backend owns authentication, pairing, operation ordering, replay, and snapshots. Android owns touch capture, optimistic rendering, local Room/DataStore persistence, wallpaper rendering, and opportunistic recovery.
Important
The wallpaper surface is display-only. Android does not allow this app to provide reliable interactive drawing from the wallpaper surface or unrestricted background networking while the device is asleep.
.
├── apps
│ ├── backend # Fastify + TypeScript API, WebSocket sync, Postgres migrations
│ ├── mobile # Android app built with Kotlin, Compose, Hilt, Room, WorkManager
│ └── web # Next.js landing site with ShadCN/Tailwind styling
├── docs
│ ├── product-prd.pdf # Product requirements and platform constraints
│ └── implementation-plan.pdf
├── lib
│ └── banner.png # README banner
├── scripts
│ └── reset-local-onboarding.sh
├── pnpm-workspace.yaml # PNPM workspace package map
├── turbo.json # Turborepo task pipeline
├── docker-compose.yml # Local Postgres + backend
└── .env.example
- Docker Desktop or another Docker Compose runtime
- Node.js 22+ and PNPM 10+ for monorepo development outside Docker
- Android Studio or Android SDK command-line tools
- JDK 17 or a compatible Android Gradle Plugin runtime
- A Firebase project if you want real FCM behavior
- A Google OAuth server client ID for real Google Sign-In
Clone the repository, then create a local environment file:
cp .env.example .envStart the local backend and database from the repository root:
docker compose up --buildThe compose stack starts:
- PostgreSQL on
localhost:5432 - Backend API on
localhost:8080
Check the backend:
curl http://localhost:8080/healthRun the Android dev build from apps/mobile:
./gradlew :app:installDevDebugThe devDebug variant points at http://10.0.2.2:8080/, so an Android emulator can reach the Dockerized backend without additional network configuration.
The backend reads configuration from environment variables:
| Variable | Required | Description |
|---|---|---|
PORT |
No | API port. Defaults to 8080. |
DATABASE_URL |
No | PostgreSQL connection string. Docker Compose sets this automatically. |
ALLOW_DEV_GOOGLE_TOKENS |
No | Allows development Google tokens when true. Defaults to false when NODE_ENV=production; local Compose enables it. |
GOOGLE_SERVER_CLIENT_ID |
For real auth | OAuth server client ID used to verify Google ID tokens. |
FIREBASE_SERVICE_ACCOUNT_PATH |
For FCM | Path to a Firebase service account JSON file. |
FIREBASE_SERVICE_ACCOUNT_JSON |
For FCM | Inline Firebase service account JSON. |
SUPABASE_URL |
For wallpaper storage | Supabase project URL for wallpaper admin and catalog storage access. |
SUPABASE_SERVICE_ROLE_KEY |
For wallpaper storage | Supabase service-role key used by the backend for wallpaper management. |
SUPABASE_WALLPAPER_BUCKET |
For wallpaper storage | Bucket name used to store wallpaper assets. |
WALLPAPER_ADMIN_PASSWORD |
For wallpaper admin | Admin header secret required by the wallpaper management routes. |
CANNY_SSO_PRIVATE_KEY |
For in-app feedback | Private Canny SSO key used by the backend to sign widget users. |
For real Google Sign-In, update .env:
GOOGLE_SERVER_CLIENT_ID=your-google-oauth-server-client-id
For Firebase Cloud Messaging, provide either FIREBASE_SERVICE_ACCOUNT_PATH or FIREBASE_SERVICE_ACCOUNT_JSON. Without Firebase credentials, the backend uses a no-op sender so local foreground sync still works.
The Android app has two product flavors:
| Flavor | API base URL | Debug menu |
|---|---|---|
dev |
http://10.0.2.2:8080/ |
Enabled |
prod |
PROD_API_BASE_URL, default https://api.mulberry.my/ |
Disabled |
GOOGLE_SERVER_CLIENT_ID and PROD_API_BASE_URL can be provided through apps/mobile/local.properties or the shell environment:
GOOGLE_SERVER_CLIENT_ID=your-google-oauth-server-client-id
PROD_API_BASE_URL=https://your-production-api.example/If you are building from a clean checkout, add the Firebase client config expected by each flavor you build:
apps/mobile/app/src/dev/google-services.json
apps/mobile/app/src/prod/google-services.json
The web app reads its public backend API URL from:
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
For local web development, copy apps/web/.env.example to apps/web/.env if you need to override the default local backend URL.
Fly.io is the canonical production deployment target for the backend configuration in this repository. The Fly app uses the existing root Dockerfile, serves the same public API contract at https://api.mulberry.my/, and keeps Supabase as the source of truth for Postgres and wallpaper storage.
Use the checked-in Fly config:
Install JavaScript workspace dependencies from the repository root:
pnpm installRun all Turborepo build tasks:
pnpm buildpnpm --filter @mulberry/backend devUseful backend commands:
pnpm --filter @mulberry/backend build
pnpm --filter @mulberry/backend testFly.io deployment uses the root Dockerfile. During migration, Railway can continue to use the same image and public domain while DNS rollback remains in place. The Docker image uses the root PNPM workspace lockfile and builds only @mulberry/backend, so the backend deployment path stays independent from the web and Android apps.
pnpm --filter @mulberry/web devUseful web commands:
pnpm --filter @mulberry/web build
pnpm --filter @mulberry/web typecheckpnpm --filter @mulberry/mobile build
pnpm --filter @mulberry/mobile install:dev
pnpm --filter @mulberry/mobile testFrom the repository root, reset the local Docker database:
docker compose down -v
docker compose up --buildFrom the repository root, reset local Android onboarding state and the Docker database:
./scripts/reset-local-onboarding.shThe backend exposes a small REST API plus one WebSocket endpoint:
| Area | Endpoints |
|---|---|
| Auth | POST /auth/google, POST /auth/refresh, POST /auth/logout |
| Devices | POST /devices/fcm-token, DELETE /devices/fcm-token |
| Bootstrap | GET /bootstrap |
| Profile | PUT /me/profile |
| Pairing | POST /invites, POST /invites/redeem, POST /invites/:inviteId/accept, POST /invites/:inviteId/decline |
| Canvas | GET /canvas/ops, GET /canvas/snapshot, GET /canvas/sync |
| Health | GET /health |
Canvas operations are stored as an ordered log. The server assigns authoritative revisions, clients apply operations in revision order, and recovery starts from the last applied revision.
Run backend tests:
pnpm --filter @mulberry/backend testRun Android unit tests:
pnpm --filter @mulberry/mobile testThe current test coverage includes route handling, app bootstrap resolution, feature flags, drawing stroke behavior, wallpaper placement/status logic, sync JSON parsing, and recovery policy behavior.
Contributions are welcome. Read CONTRIBUTING.md before opening a pull request, especially for changes that touch product direction, privacy, security, backend API contracts, or Android release behavior.
This repository intentionally keeps the root and workspace packages marked as private to prevent accidental npm publishing.
Please do not open public issues for vulnerabilities. Report security concerns using the process in SECURITY.md.
Mulberry is open source under the MIT License.
Use the devDebug flavor. It is configured for http://10.0.2.2:8080/, which maps the Android emulator back to the host machine.
Confirm that the same GOOGLE_SERVER_CLIENT_ID is available to both the backend and Android build. For quick local backend development, ALLOW_DEV_GOOGLE_TOKENS=true is already set in docker-compose.yml.
This is expected under Android Doze, app standby, and OEM battery policies. Active foreground sessions use WebSocket sync; idle devices catch up through FCM and recovery when Android grants execution time.
docker compose down -v
docker compose up --build