From Bottle Blue LLC
Your camera. Your photos. Your cloud.
Roost is a security camera you host yourself. A Raspberry Pi and a USB camera take the picture, an AI model describes and tags what it sees, and every frame lands in a private store you can search by its contents. No camera subscription, no hub, and every piece of it is yours to swap.
- 4Kcapture
- MITlicensed
- Nonesubscription
- 4moving parts
The pipeline
Four small parts, end to end
Every stage is a separate program you can read in an afternoon, and each one does a single job.
Capture
Rust · v4l
Opens the USB camera, streams MJPG, stamps the time onto each saved frame, and listens for JSON commands on two WebSocket ports.
Upload
Python · boto3
Watches the output folder, moves each new frame into a private S3 bucket, and deletes the local copy only once the upload has succeeded.
Recognize
Lambda · Claude
Fires on every new object in the bucket. Claude describes the scene and returns open vocabulary tags, and both are written to DynamoDB.
Search
React · Flask
Runs anywhere with network access to both. Drives the camera, shows the gallery, and finds a photo by what is in it.
Yours, in the way that matters
The photos are in your storage, the keys are yours, and every line of it is on GitHub. As the guide builds it, Claude sees each frame in order to describe it and nothing else does. Change that piece and nothing sees it but you.
Private by construction
The bucket blocks all public access and keeps versions, and the dashboard reaches a photo through a short lived presigned URL, so no frame is ever world readable. Every component gets its own scoped key, and the guide never asks for a root credential.
Search by what is in the frame
Claude returns open vocabulary tags rather than a fixed category list, and the table indexes them. Every photo with a delivery truck in it, newest first, is one query.
Anything can pull the trigger
The control socket takes JSON commands and also watches for a trigger file on disk, so a motion sensor, a doorbell, a garage door or a webhook can make the camera react without knowing anything about it.
Built to survive a reboot
A systemd unit starts the capture app on boot and restarts it on failure, and the camera is found by its USB path rather than a device number that can change between boots.
Real 4K at a usable frame rate
MJPG capture against libjpeg-turbo built from source, up to 3840 by 2160 at quality 95, because most generic USB cameras crawl in raw formats and only reach their rated speed in MJPG.
Swap any piece of it
It is MIT licensed and it is four small programs. The recognition step is one file making one call, and the storage is a script that writes to a bucket. Point them at a model you run and a disk you own, and nothing leaves your network and nothing bills you monthly.
One line takes a picture
The camera is a socket that accepts JSON. There is no SDK to install and no API key to send.
# ws://roost.local:8080
{"command": "snapshot"}Port 8080 drives the camera: a snapshot, a burst of them, a resolution change, a shutdown. Port 8081 starts and stops the stream. The full command set is in the capture app.
Your data, your queries
There is no API in front of your photos, because they are already yours. You hold the table and the bucket, and you read them the way you would read any of your own.
One indexed query answers every photo carrying a tag, newest first, because the tag rows are written with the tag in the index partition key.
import boto3
from boto3.dynamodb.conditions import Key
table = boto3.resource("dynamodb").Table("Roost")
partition = "USER#you#TAG#dog"
found = table.query(
IndexName="GSI1",
KeyConditionExpression=Key("GSI1PK").eq(partition),
ScanIndexForward=False,
Limit=2,
)[
{
"image_key": "camera_frame_38858.jpg",
"camera_id": "cam1",
"timestamp": "2026-07-19T14:02:11"
},
{
"image_key": "camera_frame_34775.jpg",
"camera_id": "cam1",
"timestamp": "2026-07-18T09:41:50"
}
]The table layout, the command set and every line that produces this live in the Roost repository.
What it takes to build one
An evening, maybe two. The guide calls it an advanced build because there is a lot of it, not because any one step is hard.
A Raspberry Pi, a card and power
A Pi 5 is the recommendation and 1GB of memory is plenty, plus a microSD card and USB power. The build guide is headless throughout, so wired Ethernet and SSH are all you need to reach it. No HAT, hub or proprietary module anywhere in the build.
A USB camera
Anything that speaks UVC or V4L2, which is most of them. The capture app identifies it by USB path rather than device number, so it survives a reboot that renumbers your devices.
Somewhere to put the photos
The build guide uses a private S3 bucket, a DynamoDB table and one Lambda, each with a key scoped to it. This is the piece people most often want to change, and the uploader is a short script, so pointing it at a disk you already own is a fork rather than a rewrite.
Something to look at the pictures
The guide uses Claude, which costs a fraction of a cent per photo and runs only when a frame arrives. The recognition step is one file making one call, so a model you host yourself goes in the same place.
There is no subscription and no hub, because there is no service. Follow the guide as written and you pay your own storage and a fraction of a cent for each photo. Swap those two pieces for a model you run and a disk you own, which is what the licence is for, and the running cost is your electricity and nothing at all leaves your network. The build guide takes you through every account and key the default path needs.
Build it this weekend
Start with the camera. Get one frame onto disk before worrying about sockets, buckets or dashboards. Everything after that is plumbing.