Skip to content

Repository files navigation

Mulberry banner

Mulberry

Android Kotlin TypeScript Fastify Next.js Turborepo PostgreSQL

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.

Contents

Features

  • 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.

Architecture

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"]
Loading

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.

Repository layout

.
├── 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

Prerequisites

  • 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

Getting started

Clone the repository, then create a local environment file:

cp .env.example .env

Start the local backend and database from the repository root:

docker compose up --build

The compose stack starts:

  • PostgreSQL on localhost:5432
  • Backend API on localhost:8080

Check the backend:

curl http://localhost:8080/health

Run the Android dev build from apps/mobile:

./gradlew :app:installDevDebug

The devDebug variant points at http://10.0.2.2:8080/, so an Android emulator can reach the Dockerized backend without additional network configuration.

Configuration

Backend

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.

Android

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

Web

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.

Production deployment

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:

Development commands

Install JavaScript workspace dependencies from the repository root:

pnpm install

Run all Turborepo build tasks:

pnpm build

Backend

pnpm --filter @mulberry/backend dev

Useful backend commands:

pnpm --filter @mulberry/backend build
pnpm --filter @mulberry/backend test

Fly.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.

Web

pnpm --filter @mulberry/web dev

Useful web commands:

pnpm --filter @mulberry/web build
pnpm --filter @mulberry/web typecheck

Android

pnpm --filter @mulberry/mobile build
pnpm --filter @mulberry/mobile install:dev
pnpm --filter @mulberry/mobile test

From the repository root, reset the local Docker database:

docker compose down -v
docker compose up --build

From the repository root, reset local Android onboarding state and the Docker database:

./scripts/reset-local-onboarding.sh

API overview

The 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.

Testing

Run backend tests:

pnpm --filter @mulberry/backend test

Run Android unit tests:

pnpm --filter @mulberry/mobile test

The 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.

Contributing

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.

Security

Please do not open public issues for vulnerabilities. Report security concerns using the process in SECURITY.md.

License

Mulberry is open source under the MIT License.

Troubleshooting

The emulator cannot reach the backend

Use the devDebug flavor. It is configured for http://10.0.2.2:8080/, which maps the Android emulator back to the host machine.

Google Sign-In fails locally

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.

Background updates are delayed

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.

The local database needs a clean slate

docker compose down -v
docker compose up --build

About

Paired-device Android app for two people to share a persistent drawing canvas directly on the phone's wallpaper and lock screen

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages