Skip to content

Hosted cloud sync

The hosted cloud is open to everyone as a free tier with limits (see Free tier and limits). Everything below describes the behaviour you get when you point your device at a server running the current code — including the dashboard UI this page documents.

The hosted cloud is an optional companion service for the bark monitor. Your device keeps working exactly as before — recording, bots, local files — and in addition pushes its events to the cloud, which mirrors them and shows a dashboard with the history.

The defining property: the cloud is a verified mirror, never the source of truth. All pushes are one-way (device → cloud). If the cloud is down, or you cancel it, or you never had it: your recording.db is unaffected and remains complete.

What is synced

  • Bark, activity and recording start/stop events — everything in the event store (see where your data lives). Day totals shown on the dashboard are derived from these events.
  • Audio recordings stay on the device by default. Uploading them is a separate, opt-in consent per device (off by default) — see Audio (FLAC, consent-gated).
  • The cloud re-computes the hash chain of every batch it receives: a tampered or corrupted push is rejected, not silently stored.

Pairing a device

Each device is paired once with a one-time pairing code. The GUI way — recommended:

  1. On the dashboard, create a device. You get a pairing link (and the 8-character code inside it).
  2. On the device, open the pairing wizard:
  3. Snap on a desktop: click Bark Monitor — Pair with cloud in the applications menu — the wizard opens in the browser, no terminal involved.
  4. Snap, headless (a Pi): the wizard runs on a fixed port — from any device on the same network open the Pi's address, e.g. http://raspberrypi.local:8899 (or its IP), and paste the code. No SSH needed.
  5. Snap, headless, address unknown: the exact URL (also when the port had to fall back to a random one) is in a plain file next to the config, readable without an admin password (the file is removed once paired): cat /var/snap/bark-monitor/current/pairing-url.txt. The same URL is in the service logs (snap logs bark-monitor.bark-monitor-cloud-record), but on some systems that command asks for an admin password.
  6. Docker (compose): same idea — the container prints the wizard URL in docker compose logs.
  7. Pip / desktop: run bark-monitor-cloud-record, or bark-monitor-cloud-pair-gui (on a Pi with a desktop it is also in the applications menu as Bark Monitor — Pair with cloud).
  8. Paste the pairing link (or just the code — the wizard then asks for the cloud address) and click Pair.

No config file needs to exist beforehand: the recorder creates one with default folders on first start, and the wizard fills in the cloud credentials.

The wizard exchanges the code for a long-lived device token and saves it in your config file under cloud_parameters, then recording starts by itself (under the snap service or docker; as a foreground command the recorder you launched carries on). Re-running the wizard re-pairs (useful after a reinstall: a stable machine id stored next to the config makes the device map back to the same cloud device). Pairing a new device entry from the same machine moves the machine there: the previous entry loses its recorder token and becomes an empty slot you can delete on the dashboard or re-pair with a different machine.

On a headless machine (a Pi over SSH), either keep the wizard on localhost and open the printed URL through an SSH tunnel, or use the CLI instead:

bark-monitor-cloud-pair \
    --config-file path/to/parameters.json \
    --server-url https://cloud.example.com \
    --pairing-code ABC23456

Both paths write the config exactly the same way.

Resetting the pairing from the cloud

A paired device can be re-paired without touching it — including headless recorders running under the snap service or docker. On the dashboard, open the device and click Reset pairing…: its token stops working immediately, and the recorder notices on its next poll, stops recording, and starts the pairing wizard again (its URL lands in the same places as at install time — pairing-url.txt next to the config and the service logs). Paste the new pairing code — from the same account or from a different account or server entirely — and recording resumes there. The device's local history stays on the machine; only new events go to the newly paired cloud.

Enabling the sync

With cloud_parameters containing a server_url and a device_token in the config file, the cloud sync is active. It combines with the other sync options: you can keep your Nextcloud backup and push to the hosted cloud at the same time.

Failures are non-fatal by design: if the cloud is unreachable, the events stay on the device and are pushed later (the device remembers what the cloud last acknowledged). Cloud problems never interrupt the monitoring itself.

The server side (what you see at the server_url) is the cloud dashboard: a sign-in-by-email web app showing today's barking, history charts, and the paired devices. See the dashboard README in bark_monitor_cloud/dashboard/ for what it looks like.

Running your own server

The server is a small FastAPI app (package bark-monitor-cloud). To try it locally:

uv sync --package bark-monitor-cloud
python -m bark_monitor_cloud.scripts.serve            # serves 127.0.0.1:8000
python -m bark_monitor_cloud.scripts.serve 0.0.0.0 8080  # custom host/port

The PORT environment variable is honoured as the default port (handy for PaaS deploys). uvicorn bark_monitor_cloud.main:app works too.

Changing ports

The backend port, three ways (all equivalent):

python -m bark_monitor_cloud.scripts.serve 127.0.0.1 8001   # positional [host] [port]
PORT=8001 python -m bark_monitor_cloud.scripts.serve         # env var
uvicorn bark_monitor_cloud.main:app --port 8001  # uvicorn's own flag

While hacking on the dashboard (npm run dev), the frontend and the backend are two processes, so there are two ports:

npm run dev -- --port 5174                       # the Vite dev server
VITE_API_TARGET=http://127.0.0.1:8001 npm run dev  # proxy → backend on 8001

The -- before --port is npm's separator: without it npm swallows the flag. VITE_API_TARGET tells the dev proxy where the API lives (default http://127.0.0.1:8000) — set it whenever the backend is not on 8000, or npm run dev will proxy into the void. The built bundle has no such problem: dist/ is served same-origin by the backend.

[Errno 98] address already in use means another process owns the port — often a leftover server. Find it with ss -tlnp | grep :8000 (kill by the reported pid) or just pick another port.

Environment variables

Variable Meaning Default
BARK_CLOUD_SECRET_KEY JWT signing key for sessions unset: random per process
BARK_CLOUD_DATABASE_PATH SQLite file location bark_monitor_cloud.db
BARK_CLOUD_SMTP_URL SMTP relay for magic links, smtp://user:pass@host:587 unset: link printed to server log
BARK_CLOUD_SMTP_FROM From: address for the emails bark monitor <noreply@…>
BARK_CLOUD_BASE_URL Origin for links in emails unset: request host
BARK_CLOUD_FORWARDED_ALLOW_IPS Proxy trust for X-Forwarded-For unset
BARK_CLOUD_FREE_TIER_RETENTION_DAYS Free-tier history window in days; 0 keeps everything (self-hosters) 3
BARK_CLOUD_WAITLIST_URL External signup page for the waitlist; unset = the cloud's own mailing waitlist (/#/waitlist) unset
BARK_CLOUD_AUDIO_STORAGE r2 to store recordings in Cloudflare R2 unset: local disk
BARK_CLOUD_R2_ACCOUNT_ID R2 account id unset
BARK_CLOUD_R2_ACCESS_KEY_ID R2 token access key id unset
BARK_CLOUD_R2_SECRET_ACCESS_KEY R2 token secret unset
BARK_CLOUD_R2_BUCKET R2 bucket name unset
BARK_CLOUD_R2_PUBLIC_BASE Custom R2 endpoint domain unset: <account>.r2.cloudflarestorage.com
BARK_CLOUD_R2_PRESIGN_TTL_SECONDS Lifetime of presigned R2 upload/download URLs 900 (15 min)
BARK_CLOUD_MATRIX_HOMESERVER Homeserver of the Matrix bot; unset disables the bot unset
BARK_CLOUD_MATRIX_USER_ID The bot's Matrix user id unset
BARK_CLOUD_MATRIX_PASSWORD The bot's Matrix password unset
BARK_CLOUD_MATRIX_NOTIFY_MIN_INTERVAL_SECONDS Min seconds between bark notifications per user 300
BARK_CLOUD_MATRIX_NOTIFY_INTERVAL_SECONDS How often the bot checks for new barks (notification latency floor) 10.0
BARK_CLOUD_EMAIL_DIGEST_CHECK_INTERVAL_SECONDS How often the digest loop checks who is due (each user gets one email per day at their chosen local time) 300
BARK_CLOUD_COMMAND_TTL_SECONDS Recorder commands older than this are dropped 600

BARK_CLOUD_SMTP_URL picks its TLS mode from the port and an optional ?tls= query: starttls on 587 (default), ssl on 465, ?tls=none for a local relay.

Signing in (dev)

There are no passwords: enter your email, and the server delivers a one-time link. Without BARK_CLOUD_SMTP_URL the "delivery" is a line in the server terminal:

[magic-link] you@example.com: http://127.0.0.1:8000/#/verify?token=…

Paste it into a browser and you are signed in. Each link works once and expires after 30 minutes. With SMTP configured the same link arrives by email and the dashboard says so.

The dashboard itself is a separate Vite dev server while hacking on the UI (npm run dev inside bark_monitor_cloud/dashboard/, proxying API calls to the backend) — ports and the proxy target are covered in the dashboard README.

Secrets and sessions

Session tokens are JWTs signed with BARK_CLOUD_SECRET_KEY. Without it, a random key is generated once per process: sessions work fine but everyone signs in again after a restart (and each worker in a multi-worker deployment would need it set, or they would each mint their own key — so production must set it):

export BARK_CLOUD_SECRET_KEY=$(openssl rand -hex 32)

Demo data

bark_monitor_cloud/bark_monitor_cloud/scripts/seed_demo_data.py fills an account with three weeks of realistic, hash-chain-verified events through the real API (magic link, device pairing, verified ingest) — the same bytes a Raspberry Pi would push:

uv run --package bark-monitor-cloud \
    python bark_monitor_cloud/bark_monitor_cloud/scripts/seed_demo_data.py \
    --email test@bark.com --db bark_monitor_cloud.db

Then sign in from the dashboard with that email. Re-running wipes and re-seeds the account's devices.

Free tier and limits

There is no paid tier and no billing: the hosted cloud is a free tier, open to everyone, with limits that fit on one small server run by one human. Two roles exist (users.role), and every limit is enforced server-side through a single function (get_limits in bark_monitor_cloud/limits.py) — the dashboard and the CLI read the same numbers:

user (free, default) owner (the operator)
Devices 1 unlimited
Cloud history 3 days unlimited
Audio upload (FLAC, consent-gated) not allowed allowed
Evidence PDF report not allowed allowed

The rationale is what one small server can hold with headroom: a 3-day mirror per account keeps SQLite and disk usage tiny, one device covers the typical case (a Pi by the window), and audio plus evidence reports are the expensive bits — storage and CPU — so they stay with the operator until there is a reason to offer more. None of it limits the device itself: your recording.db always keeps everything, the cloud is a mirror, never the source of truth.

The waitlist door

When a limit is hit, the API answers a structured limit_exceeded error (naming the limit) and the dashboard shows a banner; both carry a human-friendly message pointing at the waitlist — a mailing waiting list, so nobody has to open a repository issue to ask for more.

The list is part of the cloud itself, deliberately simple:

  • Joining: /#/waitlist on the dashboard — a plain email form, no account needed (POST /waitlist for anything else that wants to point at it). One row per email, friendly re-subscriptions, no verification mail.
  • Reading it: the list is the operator's. GET /api/waitlist (owner role) returns what has been collected, oldest first — or read the waitlist table directly in the database.
  • The cloud never sends mail from here. When there is something to tell the list, the operator exports the addresses and mails from wherever they already send mail.

Already running a mailing list elsewhere? Point BARK_CLOUD_WAITLIST_URL at its signup page and every waitlist pointer — banner, landing page, limit messages — uses that instead.

Roles are not for sale (or for the API)

users.role is written only by the operator, directly in the database — no API endpoint can change it (request bodies reject unknown fields with a 422, and no code path touches the column; the test suite enforces both). Grant yourself unlimited access on a self-hosted server the same way:

UPDATE users SET role = 'owner' WHERE email = 'you@example.com';

The billing-era tables (subscriptions, payments) and the users.tier column still exist in the database for schema history, but nothing reads or writes them anymore.

Retention (free-tier history)

The 3-day free history is enforced on the cloud mirror only — the device store always keeps everything. Every event push prunes that user's events older than the window (pruning at ingest: no cron job, no extra service), and the ack reports how many events the mirror kept and dropped. BARK_CLOUD_FREE_TIER_RETENTION_DAYS overrides the window (self-hosters: set it to 0 to keep everything; the operator role is always unlimited).

Deleting events from a hash chain would normally break verification, so the pruner records a chain anchor per device: the hash the first surviving event chains from. Verification (ingest segment checks and the dashboard integrity endpoint) starts from the anchor instead of true genesis, so a pruned account still shows verified: true — honest meaning: the retained chain verifies.

Two consequences worth knowing:

  • Re-pushing old events after a prune is fine: the cloud verifies the suffix that extends its head and skips the rest.
  • The overlap a device re-sends must still hash-verify on its own; tampering with pruned history is rejected just the same.

Audio upload is opt-in per device (GDPR) and an owner-only feature (free tier: the cloud answers 403 with a structured limit_exceeded naming the limit — the device keeps its recordings locally, which is already the default behaviour, so nothing is lost). The consent toggle on the Devices page stays off by default. The flow:

  1. The device converts its WAV recordings to FLAC (lossless, roughly half the size) and asks the cloud where to PUT it (POST /ingest/audio-url, keyed by the activity event's chain hash).
  2. Without consent the cloud answers 403 and the device marks the recording done locally — it never retries, so an opted-out device does not spam the server.
  3. With consent (and the owner role), the upload goes to whichever storage is configured:

  4. local disk (default): files under <db dir>/audio/, served by the authenticated /api/audio/<device>/<hash> route.

  5. Cloudflare R2 (BARK_CLOUD_AUDIO_STORAGE=r2 + the four BARK_CLOUD_R2_* settings): the device gets a SigV4-presigned PUT URL (15 min) and uploads directly to the bucket — the bytes never pass through the app. Downloads redirect to a presigned GET.

  6. Withdrawing consent deletes every stored recording for that device, rows and bytes. Deleting the device does the same.

The presigned URLs are computed with a stdlib-only SigV4 implementation (no AWS SDK); R2 speaks the S3 dialect.

Deleting your data

Deleting a device on the dashboard deletes every event mirrored for it, and resets the pairing. Since the device is the source of truth, a device that pushes again afterwards starts a fresh history on the cloud (nothing is "restored" from the local store beyond what is recorded after the deletion).