Raspy is a personal Raspberry Pi control plane: one FastAPI backend, a server-driven Svelte shell, and a set of small "attachments" that add apps without changing the frontend.
The backend is the spine. It owns auth, sessions, the attachment registry,
SQLite storage, WebSocket events, notifications, and the static frontend bundle.
Each attachment contributes an API router and a declarative UI descriptor. The
frontend downloads the manifest, renders whatever apps the current account can
see, and talks back to each app under /api/att/<id>/.
backend/- Python 3.13 FastAPI app, managed withuv.frontend/- SvelteKit static SPA, managed withbun.scripts/- frontend build and preview helpers.systemd/- service file for running the spine on a Pi.plan/- architecture notes and roadmap.
Built-in attachments currently include accounts, todo, notes, notifications,
files, system stats, mail, calendar, contacts, vault, connectivity, and
vibe-of-the-day. The exact list is discovered at startup and filtered per account
in /api/manifest.
The fastest path needs nothing pre-installed — no Python, no Node. A self-contained binary (built by CI for each platform) is downloaded, verified, and set up as a boot service. The installer also creates the first admin account and generates Web Push keys for you.
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/vardhin/raspy/master/scripts/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/vardhin/raspy/master/scripts/install.ps1 | iexRe-running the installer on a machine that already has Raspy shows a menu: update, uninstall, or cancel. Uninstall removes the binary and boot service and asks before touching your data dir (accounts, vault, notes).
After it finishes, open http://127.0.0.1:49317 and log in with the admin
account you just created. To go further (build from source, dev frontend), read
on.
Raspy is no longer just a sketch. The repo has:
- password login plus mini-PIN unlock, Argon2id storage, rotating sessions, CSRF protection, lockout/rate-limit behavior, and account-scoped app visibility;
- an optional browser-to-spine encrypted channel layered over HTTP requests;
- a WebSocket event bus for live app updates;
- foreground notification history plus optional Web Push via VAPID keys;
- per-attachment SQLite tables and data directories;
- a static Svelte shell served by FastAPI in production;
- a generic renderer for declarative attachment UIs, plus richer native Svelte views for apps that need them.
The project is still personal infrastructure, so assume the planning docs may describe future work as well as completed work. The source is the authority.
Install tools:
# backend
curl -LsSf https://astral.sh/uv/install.sh | sh
# frontend
curl -fsSL https://bun.sh/install | bashInstall backend dependencies and create the first account:
cd backend
uv sync
uv run raspy-auth create-accountStart the spine:
uv run raspyBy default it binds to 127.0.0.1:49317. Health is public at
http://127.0.0.1:49317/api/healthz. Protected APIs, including
/api/manifest, require login.
Run the backend in one terminal. In another:
cd frontend
bun install
cp .env.example .envSet PUBLIC_API_BASE in frontend/.env when the frontend runs on a different
origin than the spine:
PUBLIC_API_BASE=http://127.0.0.1:49317Then start Vite:
bun run devOpen the Vite URL, usually http://localhost:5173.
The production frontend is static. Build it, then let the spine serve
frontend/build/ from the same origin as the API:
scripts/build-frontend.sh
cd backend
uv run raspyFor local preview of the static bundle without the Python spine serving it:
scripts/build-and-serve.shRuntime state lives under backend/data/ by default and is gitignored. Override
with environment variables or data/config.toml.
Common settings:
RASPY_HOSTandRASPY_PORT- bind address for the spine.RASPY_DATA_DIR- SQLite, secrets, and attachment data.RASPY_STATIC_DIR- built frontend directory to serve.RASPY_ATTACHMENTS_DIR- optional drop-in attachment packages.RASPY_DISABLED_ATTACHMENTS- disable discovered attachments by id.RASPY_VAPID_PUBLIC_KEY,RASPY_VAPID_PRIVATE_KEY,RASPY_VAPID_SUBJECT- enable background Web Push notifications.
Useful auth commands:
cd backend
uv run raspy-auth calibrate
uv run raspy-auth set-pin --username <name>
uv run raspy-auth reset-password --username <name>
uv run raspy-auth revoke-all --username <name>
uv run raspy-auth gen-channel-keyGenerate VAPID keys for push notifications:
cd backend
uv run raspy-vapidBackend:
cd backend
uv run pytest -qFrontend:
cd frontend
bun run check
bun run buildThe provided systemd unit assumes:
- Linux user
raspberrypi; - repo cloned to
/home/raspberrypi/raspy; - backend working directory at
/home/raspberrypi/raspy/backend; uvinstalled at/home/raspberrypi/.local/bin/uv.
Install on the Pi after adjusting paths if needed:
sudo cp systemd/raspy.service /etc/systemd/system/raspy.service
sudo systemctl daemon-reload
sudo systemctl enable --now raspyThe spine is designed to sit behind LAN access, Tailscale, Cloudflare Tunnel, or another reverse layer. App auth stays inside Raspy either way.
Installed binaries self-check against the latest GitHub Release. When a newer
version exists, an Update available banner appears in the UI (admin only).
Clicking Update now downloads the new binary, verifies its checksum, swaps it
in, and restarts via the service manager — the server never restarts silently.
Source checkouts report "not updatable" and are updated with git pull as usual.
The connectivity attachment (admin only) sets up remote access from the UI:
paste a Cloudflare Tunnel token or a Tailscale auth key and Raspy brings the link
up and shows the public address. It detects whether cloudflared / tailscale
are installed and links to their install docs if not. See
plan/56-connectivity.md.
scripts/release.sh bumps the version (single source of truth:
backend/raspy/__init__.py), tags, and pushes — which triggers
.github/workflows/release.yml to build one binary per platform and publish them
plus SHA256SUMS and latest.json to a GitHub Release.
scripts/release.sh patch # or minor / major / an explicit 1.2.3
scripts/release.sh --dispatch # trigger the build without tagging (needs gh)Create a package that exposes a BaseAttachment instance. Built-ins live under
backend/raspy/attachments/; external attachments can be loaded from
RASPY_ATTACHMENTS_DIR or via the raspy.attachments entry point.
from fastapi import APIRouter
from raspy.core.contract import BaseAttachment
from raspy.core import ui
class Ping(BaseAttachment):
id = "ping"
title = "Ping"
icon = "activity"
version = "0.1.0"
def router(self) -> APIRouter:
r = APIRouter()
@r.get("/now")
async def now():
return {"pong": True}
return r
def ui(self):
return ui.view(
title="Ping",
children=[ui.button("Ping", action=ui.get("now"))],
)
attachment = Ping()Restart the spine. The app is mounted at /api/att/ping/... and appears in the
manifest without rebuilding the frontend.
Start with plan/README.md, then read the backend and frontend READMEs for implementation-specific notes.