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:
- On the dashboard, create a device. You get a pairing link (and the 8-character code inside it).
- On the device, open the pairing wizard:
- Snap on a desktop: click Bark Monitor — Pair with cloud in the applications menu — the wizard opens in the browser, no terminal involved.
- 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. - 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. - Docker (compose): same idea — the container
prints the wizard URL in
docker compose logs. - Pip / desktop: run
bark-monitor-cloud-record, orbark-monitor-cloud-pair-gui(on a Pi with a desktop it is also in the applications menu as Bark Monitor — Pair with cloud). - 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:
/#/waitliston the dashboard — a plain email form, no account needed (POST /waitlistfor 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 thewaitlisttable 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 (FLAC, consent-gated)
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:
- 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). - Without consent the cloud answers
403and the device marks the recording done locally — it never retries, so an opted-out device does not spam the server. -
With consent (and the owner role), the upload goes to whichever storage is configured:
-
local disk (default): files under
<db dir>/audio/, served by the authenticated/api/audio/<device>/<hash>route. -
Cloudflare R2 (
BARK_CLOUD_AUDIO_STORAGE=r2+ the fourBARK_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. -
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).