Sheaf

Self-Hosting

Sheaf is designed to be self-hosted. If you've ever set up a Minecraft server or a Discord bot, you can handle this.

What you need

That's it. Postgres and Redis come with the Compose stack, and the all-in-one image below handles TLS for you.

Quick start (all-in-one)

For a small single-instance self-host, the all-in-one image bundles the backend, the web UI, and a Caddy reverse proxy in one container, alongside Postgres. It generates its own secrets on first start and fetches and renews a Let's Encrypt certificate automatically, so a public HTTPS deploy is close to a one-liner:

git clone https://github.com/sheaf-project/sheaf.git
cd sheaf
cp .env.example .env
# Set AIO_DOMAIN=sheaf.example.com in .env, and point that domain's DNS at this box
docker compose -f docker-compose.aio.yml up -d

Publish ports 80 and 443 and you're done. Leave AIO_DOMAIN unset to serve plain HTTP on port 80 for a LAN-only instance or one sitting behind your own TLS proxy.

No public IP, behind CGNAT, or can't forward ports? The image bundles cloudflared, so setting CF_TUNNEL_TOKEN serves the instance to the internet over an outbound-only connection - nothing to forward, and Cloudflare terminates TLS. You need a domain whose DNS is managed by Cloudflare; a free plan is fine.

The all-in-one is deliberately scoped to small deployments and doesn't scale horizontally. If you outgrow it, move to the split images below - same env vars, same data.

Quick start (split images)

For anything you want to scale, or if you'd rather run your own reverse proxy:

git clone https://github.com/sheaf-project/sheaf.git
cd sheaf
cp .env.example .env
# Edit .env - at minimum, change POSTGRES_PASSWORD and JWT_SECRET_KEY
docker compose up -d

The API is then available at http://localhost:8000 with interactive docs at http://localhost:8000/v1/docs. You build and serve the frontend and terminate TLS yourself.

/health is an always-200 liveness probe; /health/ready checks the database and Redis under a tight timeout, for use as a readiness gate in front of a rolling deploy.

Generating secrets

# JWT secret
python -c "import secrets; print(secrets.token_urlsafe(32))"

# Encryption key (optional - auto-generated on first start if not set)
python -c "import secrets; print(secrets.token_hex(32))"

Important: if you let Sheaf auto-generate the encryption key, it's saved to data/encryption.key inside the Docker volume. Back this up. If you lose it, all encrypted data (emails, TOTP secrets) is unrecoverable.

Configuration

The full self-hosting guide in the repo covers:

Read it at docs/SELFHOSTING.md.

Mobile push notifications

This is the most-asked-about config that doesn't work the way most people first guess, so it gets its own section.

The constraint

Sheaf supports a mobile_push notification channel type that fans out to a recipient's registered iOS / Android devices. For mobile push to actually reach the phone, the server that's sending the push has to hold credentials matching the app installed on that phone. Both Apple and Google enforce this:

So the published Sheaf apps you install from the App Store and Play Store are bound at build time to Lupine Systems' push infrastructure. A self-hosted Sheaf instance can't simply "add its own Firebase credentials" and reach those installed apps. There's no version of the published app that listens to credentials configured at runtime.

What this means for you

You have three options, in roughly increasing order of effort:

Sheaf supports four delivery types that don't require any mobile-app coordination at all:

For 95% of self-hosters this is the right answer: it works today with no extra setup, no app store accounts, and no ongoing maintenance burden.

Pointing a webhook or ntfy at your own LAN. Sheaf blocks outbound delivery to private, loopback, and link-local addresses by default, because otherwise any account on your instance could use your server to probe your internal network. If you want to drive a local Home Assistant, Node-RED, or self-hosted ntfy, list the specific ranges you'll allow in WEBHOOK_ALLOWED_PRIVATE_CIDRS (e.g. 192.168.1.0/24). Cloud metadata endpoints stay blocked regardless, and a target that would be rejected is reported when you save the channel rather than silently failing on first delivery. Only turn this on if you trust everyone with an account on your instance. It's ignored entirely on the hosted service.

Pulling instead of pushing. If the consumer can hold a connection open, GET /v1/fronts/stream avoids all of this - your automation dials out to Sheaf and receives front changes live, so nothing needs to reach into your network at all.

Running Home Assistant? The Sheaf integration supports all three transports and defaults to polling, but the live stream is the one to pick: it needs no inbound reachability, so you can skip the CIDR allowlist entirely and leave the SSRF protection strict. Only reach for the webhook transport if you have a specific reason to.

Option 2: Fork and republish the mobile apps (advanced)

Nothing about Sheaf is hostage to Lupine's app store accounts. The Android and iOS app source code is open (android, ios, AGPL-3.0). To run your own end-to-end mobile push for your own instance, you would:

  1. Apple side:
    • Enrol in the Apple Developer Program ($99/year). Requires a real legal identity; organisations need a D-U-N-S number.
    • Register a new bundle ID for your fork (e.g. org.example.sheaf).
    • Create an APNs Authentication Key under that team (Certificates, Identifiers & Profiles → Keys). Download the .p8 file (you only get one chance) and note the Key ID and Team ID.
    • Set APNS_TEAM_ID, APNS_KEY_ID, APNS_BUNDLE_ID (your new bundle), and the path to the .p8 in your instance's environment.
    • Build the iOS app against your new bundle ID, sign it with your team's distribution certificate, submit it to the App Store (or distribute via TestFlight / ad-hoc / enterprise channels).
  2. Android side:
    • Create a Firebase project in the Google Cloud Console.
    • Add an Android app to that project with a new package name (e.g. org.example.sheaf).
    • Download the google-services.json and drop it into the Android repo at build time.
    • Generate a service-account JSON under that Firebase project with the Firebase Cloud Messaging API role. Mount it in the instance and point FCM_CREDENTIALS_PATH at it.
    • Enrol in the Google Play Developer Program ($25 one-time). Generate an upload key, build the AAB, submit it to the Play Console. Google Play App Signing will hold the distribution key; record both the upload and distribution SHA-256 cert fingerprints.
    • Optionally: also publish via IzzyOnDroid, F-Droid, or direct APK distribution. You can sign all of these with the same upload key.
  3. Site-side (sheaf.sh equivalent):

This is real, ongoing work. App Store / Play Store reviews, key rotation, certificate renewal (APNs keys don't expire, but APNs certificates do; if you go the certificate route instead of key-based auth, that's a yearly chore). For most self-hosters running an instance for themselves and a small group of trusted users, the effort dwarfs the value over just using web push.

Option 3: Future - a shared Sheaf push relay (not yet built)

A "you don't have to fork the apps to get mobile push" path is something we'd like to eventually provide as a paid Lupine-operated service. Self-hosted instances would webhook events to a relay, which would fan out to FCM / APNs using Lupine's published-app credentials. Not currently scheduled; it depends on user demand and on abuse-control infrastructure being in place first.

Architecture (where the push actually flows)

For the Lupine-published apps, the flow looks like this:

                        ┌─────────────────────────────┐
                        │   Lupine instance           │
                        │   (app.sheaf.sh, etc.)      │
                        │   - holds FCM creds         │
                        │   - holds APNs key          │
                        └────────────┬────────────────┘
                                     │ mobile_push send
                       ┌─────────────┴───────────────┐
                       ▼                             ▼
              ┌─────────────────┐           ┌─────────────────┐
              │  Google FCM     │           │  Apple APNs     │
              │  (Lupine's      │           │  (Lupine's      │
              │   project)      │           │   team / bundle)│
              └────────┬────────┘           └────────┬────────┘
                       │                             │
                       ▼                             ▼
              ┌─────────────────┐           ┌─────────────────┐
              │  Sheaf Android  │           │  Sheaf iOS app  │
              │  (Play Store)   │           │  (App Store)    │
              └─────────────────┘           └─────────────────┘

For a self-hosted instance using Option 1 (web push / ntfy / Pushover / webhook), the flow is shorter and the published Sheaf apps are not involved at all:

                ┌─────────────────────────────┐
                │   Your self-hosted instance │
                └────────────┬────────────────┘
                             │
        ┌────────────────────┼────────────────┬─────────────────┐
        ▼                    ▼                ▼                 ▼
  ┌──────────┐         ┌──────────┐    ┌──────────┐      ┌──────────┐
  │ Mozilla/ │         │ Recipient│    │ Pushover │      │  Custom  │
  │ Google/  │         │ ntfy.sh  │    │  API     │      │  webhook │
  │ Apple    │         │ topic    │    │          │      │  URL     │
  │ web push │         │          │    │          │      │          │
  └────┬─────┘         └────┬─────┘    └────┬─────┘    └────┬───────┘
       ▼                    ▼               ▼               ▼
  Browser              ntfy app on     Pushover app    Whatever the
  service worker       recipient's     on recipient's  recipient
  on any device        phone           phone           wired up

For a self-hosted instance using Option 2 (fork + republish), the flow mirrors the Lupine architecture but with your own Firebase project, Apple Team, and re-published apps in place of Lupine's. From the user's phone all the way back to your instance, every link in the chain is owned by you.

Using cloud services

Any PostgreSQL 16+ instance works - set DATABASE_URL in your .env and point Sheaf at RDS, Cloud SQL, Supabase, Neon, or similar. Same applies to Redis. S3-compatible storage (AWS S3, Backblaze B2, Cloudflare R2, MinIO) works for file uploads.

For the bundled database you don't need DATABASE_URL at all: setting POSTGRES_PASSWORD is enough, and Sheaf derives the connection string from it, so the credential can't drift between the two. Set DATABASE_URL explicitly only for an external database or custom driver options.

Running an instance for others

The AGPL allows you to run Sheaf as a public service. If you modify the code and run it publicly, you must publish your modifications under the same license.

You're allowed to charge for access. Before you do, think about your threat model: you will be handling sensitive identity data belonging to other people. The bigger your instance gets, the higher the chance you'll eventually receive a subpoena for user data. Have mechanisms in place to handle that, and seek legal advice if you're unsure.

Getting help