Cheeto Cam

A self-hosted pet camera that knows which cat it's looking at

Status
In testingIn private testing on Google Play internal testing and TestFlight, with no public store listing yet. The detector and home server run every day.
Platforms
Raspberry Pi, Android, iOS, Web
Role
Solo: hardware, detector and ML pipeline, backend and infrastructure, Flutter app, web dashboard, release
Timeline
Dec 2024 – Nov 2026267 commits
Version
App 1.6.4 (build 21), not yet uploaded; 1.5.0 (build 15) is in App Store Connect / TestFlight; Android in Play internal testing

01 The story

A pet camera I built for my orange tabby, Cheeto. A Raspberry Pi 5 spots animals with YOLO, identifies the pet with CLIP and captions the visit with Florence-2, all on the Pi. A home server wakes the app with an empty push, so no detection detail passes through Google or Apple.

Cheeto Cam started in December 2024 as a Python script on a Raspberry Pi that watched a USB webcam for one orange cat and emailed a photo when it saw him. Over the next year it picked up YOLO detection, Gemini-written captions, weather, a hosted database, Pushover alerts and a web dashboard that made time-lapse GIFs. The detection history starts in April 2025.

In mid-2026 it became a real product, now called Critter Cam, in four repos: the Pi detector (public), a Flutter app for Android and iOS, a self-hosted backend on a refurbished Lenovo ThinkCentre (Supabase-compatible Postgres, auth, realtime and storage in Docker), and the web dashboard. The home network forwards no inbound ports. The server dials out through a Cloudflare Tunnel, and live video is token-gated.

All the AI runs on one Raspberry Pi 5, and no frame goes to a cloud model. YOLO11n finds an animal. A CLIP few-shot prototype, trained first on about 930 crops of Cheeto and later on confirm and correct taps in the app, decides who it is. Florence-2 writes a short activity caption. A small, unit-tested arbiter settles disagreements: when YOLO sees a 'bear' and the caption says 'squirrel', the only thing that can make a box that big at that distance is the cat.

Push was the hardest part. A self-hosted socket behind an Android foreground service silently stopped after six hours because of an Android 15 limit, and alerts went missing while the app looked healthy. The fix was to make FCM carry nothing: the payload is {"k":"det"}. The phone wakes, fetches the newest detection straight from the home server and builds the notification on the device. iOS gets a fixed generic message from APNs directly, and a notification service extension rewrites it with the real photo and caption.

The app is at 1.6.4 (build 21) and in private testing through Google Play internal testing and TestFlight. Households share access by QR code. It handles a USB webcam plus two wireless ESP32/AMB82 cameras, a 15-second rewind, battery saver and a Pi Zero desk frame. The newest detector work added audio from the outdoor camera and ignores birds.

02 What it does

  1. 01

    Alerts that name the visitor, like 'Cheeto' or 'Unknown visitor', with a photo and a one-line activity caption

  2. 02

    Visits, not noise: repeated frames group into one visit, and you choose first-of-visit, repeat-while-staying or every-detection alerts

  3. 03

    Teach it your regulars: confirm or correct a sighting and the Pi rebuilds that pet's profile from your reviews

  4. 04

    Live view with audio, and a rewind that plays the last 15 seconds as an MP4 made on demand

  5. 05

    Multiple cameras (USB webcam, ESPHome XIAO ESP32-S3, AMB82-Mini with PIR), each with its own rotation, label and battery saver

  6. 06

    Private households: join with a 10-character code or QR, owner, manager and member roles, and a recovery key for ownership

  7. 07

    History calendar, server-side analytics, and visit, day or year-long GIFs to save or share

  8. 08

    Android widget and shortcuts, an iOS notification extension with photo previews, and a Pi Zero desk frame showing the latest visitor

03 How it's built

DeviceCameras: USB, XIAO ESP32-S3, AMB82-Mini
DevicePi 5 detector (one process per camera)
ModelYOLO11n: is an animal there?
ServicePush dispatcher (FastAPI)
ModelCLIP few-shot: which pet is it?
ModelFlorence-2: what is it doing?
Outside serviceFCM (Android)
Outside serviceAPNs (iOS)
Servicentfy (self-hosted fallback)
AppCritter Cam app (Flutter, Android/iOS)
AppWeb dashboard PWA
ServiceLive view + 15 s rewind server
Outside serviceCloudflare Tunnel (outbound only)
DeviceDesk frame (Pi Zero W + LCD)
ServiceSupabase: auth, REST, realtime, storage
DataPostgres (RLS, household RPCs, stats)

The Pi is the only device that can write detections. The home server is the only one that talks to the internet, and only outbound through a Cloudflare Tunnel. Push messages carry no content: the phone wakes, fetches the detection from the home server and builds the notification itself. Pet embeddings never leave the Pi; the database stores only references to reviewed samples.

All 19 connections, as text
  • Cameras: USB, XIAO ESP32-S3, AMB82-Mini → Pi 5 detector (one process per camera): USB, MJPEG, JPEG (motion-gated)
  • Pi 5 detector (one process per camera) → YOLO11n: is an animal there?: every frame
  • YOLO11n: is an animal there? → CLIP few-shot: which pet is it?: animal crop
  • YOLO11n: is an animal there? → Florence-2: what is it doing?: context crop
  • Pi 5 detector (one process per camera) → Supabase: auth, REST, realtime, storage: upload photos, then insert detection
  • Supabase: auth, REST, realtime, storage → Postgres (RLS, household RPCs, stats): store
  • Pi 5 detector (one process per camera) → Push dispatcher (FastAPI): notify
  • Push dispatcher (FastAPI) → FCM (Android): data-only {"k":"det"}
  • Push dispatcher (FastAPI) → APNs (iOS): generic alert
  • Push dispatcher (FastAPI) → ntfy (self-hosted fallback): parallel fallback
  • FCM (Android) → Critter Cam app (Flutter, Android/iOS): wake
  • APNs (iOS) → Critter Cam app (Flutter, Android/iOS): wake + extension rewrite
  • Critter Cam app (Flutter, Android/iOS) → Cloudflare Tunnel (outbound only): fetch newest detection, photos, stats
  • Cloudflare Tunnel (outbound only) → Supabase: auth, REST, realtime, storage: path-allowlisted ingress
  • Critter Cam app (Flutter, Android/iOS) → Live view + 15 s rewind server: token-gated live / rewind (via tunnel)
  • Pi 5 detector (one process per camera) → Live view + 15 s rewind server: shares decoded frames
  • Web dashboard PWA → Cloudflare Tunnel (outbound only): household dashboard
  • Desk frame (Pi Zero W + LCD) → Supabase: auth, REST, realtime, storage: polls detections as a household member
  • Critter Cam app (Flutter, Android/iOS) → Pi 5 detector (one process per camera): reviews become training samples (via DB)

04 Engineering notes

  • Three local models on one Raspberry Pi 5 (YOLO11n, CLIP ViT-B-32, Florence-2-base), shared by one detector process per camera through a cross-process flock semaphore, so captioning on one camera can't freeze the others

  • Few-shot identity from prototypes: a centroid plus up to six deliberately different exemplars per pet, scored 0.6/0.4, accepted only above 0.90 with a 0.015 margin over the runner-up, so two similar orange cats stay 'unknown' instead of swapped

  • Content-free push: an FCM data-only ping, then the phone fetches the detection over the tunnel. APNs is sent directly from the home server to a notification service extension, and dead tokens are pruned automatically

  • Postgres row-level security, with every client call going through a security-definer RPC that checks household membership. Aggregates run in Postgres, so opening the app costs about 17 KB of JSON

  • Rewind uses a ring buffer capped first by bytes, then by seconds, recording at 4 fps (about 1.3% of a core) and muxed with ffmpeg only on request. The mic is captured once and shared by rewind and live listeners

  • I built and tested iOS in GitHub Actions on macOS runners while developing on Windows. A Ruby script adds the notification-extension target, so there's no Xcode click-through

05 By the numbers

5,207
Detections stored, April 2025 to July 2026
1,906
Visits over 384 active days (counted at 5,192 detections)
10,415
Photos in storage (880 MB)
930
Reference crops of Cheeto in the identity training set
0
Inbound router ports opened
~17 KB
JSON pulled to open the app
15
Containers on the home server, across 3 Docker stacks
~$80
Home server: a refurbished ThinkCentre M700 Tiny

06 Stack

App

  • Flutter (Android, iOS)
  • Kotlin home-screen widget
  • Swift notification service extension
  • fl_chart
  • video_player / just_audio
  • Vanilla JS PWA dashboard

Backend

  • Python detector on Raspberry Pi 5
  • Self-hosted Supabase stack (Kong, PostgREST, GoTrue, Realtime, Storage)
  • FastAPI push dispatcher
  • ntfy
  • Firebase Cloud Messaging
  • APNs (HTTP/2, direct)

Data

  • PostgreSQL with row-level security and household RPCs
  • Supabase Storage (JPEG frames and thumbnails)
  • On-Pi embeddings and prototypes (.npz)

Infrastructure

  • Docker Compose on a Lenovo ThinkCentre M700 Tiny (Debian)
  • Cloudflare Tunnel (no inbound ports)
  • systemd units per camera
  • Nightly pull-based backups
  • Prometheus / Grafana / cAdvisor (LAN only)
  • Firebase Hosting (dashboard)
  • ESPHome and Arduino firmware for wireless cameras
  • OpenSCAD outdoor enclosure

Tooling

  • Ultralytics YOLO11n
  • open_clip ViT-B-32-quickgelu
  • Microsoft Florence-2-base (transformers)
  • OpenCV
  • ffmpeg / libx264
  • pytest
  • GitHub Actions (iOS build, TestFlight)
  • OpenWeatherMap

Lines of code (approx.)

  • Dart17,285
  • Python6,895
  • SQL4,841
  • HTML3,109
  • CSS1,135
  • Arduino C++726

≈ 36,045 lines

Repositories

  • cheetocam ↗public
  • cheetocam-appprivate
  • cheeto-infraprivate
  • cat-detectorprivate

Four repos: cheetocam 109 commits (2024-12-02 to 2026-11-09), cat-detector 54 (2025-03-02 to 2026-08-01), cheetocam-app 68 (2026-07-31 to 2026-11-09) and cheeto-infra 36 (2026-07-31 to 2026-11-09). The 2026-11-09 commits on the app and infra repos are mostly snapshot and handoff commits; the last substantial app release commit is 2026-08-11 (1.6.4). The detector's 2026-11-09 commit is real code: outdoor-camera audio, lighter live frames and ignoring birds.