TPCloud is everything I self-host, in one repo. It's the front door that every request goes through: one gateway at a single address, where every module is a path (/store, /photos, /home, /loginβ¦), one auth service that signs you in once for all of them, my storage and photo backup, the web apps for all of it and the native iOS and macOS app. Home automation lives in its own repo, tphome, because it's a project of its own with firmware and hardware, and plugs in as one more module. Anyone can run their own from one file, downloaded from tpcloud.migueltaibo.com.
Every service I added to my server needed its own login, its own port and its own way of being reached from outside. I wanted it to feel like a single cloud instead:
- One login β sign in once at
/loginand every module knows who you are - Reached your way β only at home, privately from anywhere through Tailscale, or at your own domain through a Cloudflare Tunnel; nothing is opened unless you choose to
- Plug in a module β a new service is one more process the server runs and one more path in the gateway
- Modern auth β passkeys on the web, password + TOTP for the native app, and sessions you can see and revoke
π |
π |
ποΈ |
π |
π± |
π tphomeHome automation with my own Wi-Fi switches for lights and blinds: firmware, MQTT broker and the API behind C++ Β· ESP8266 Β· MQTT Β· FastAPI Β· Vue 3 |
π§° |
β¬οΈ |
tpcloud/
βββ apple/ native iOS and macOS app (Swift package + XcodeGen project)
βββ services/
β βββ auth/ sign-in, sessions, users
β βββ gateway/ Caddyfile (+ extra/ for blocks of one server only)
β βββ store/ files, photos, share links
βββ web/
β βββ ui/ shared design system for every web app
β βββ login/ accounts/ cloud/
β βββ store/ photos/ share/
β βββ home/ tphome's control panel
βββ cli/ the `tpcloud` command: one file with everything inside
βββ site/ tpcloud.migueltaibo.com, where it's downloaded
βββ data/ what the services write when run with Docker (not in git)
βββ docker-compose.yml the same services as containers, for running with Docker instead
βββ install.sh the installer for that Docker setup
Internet Β· Tailscale Β· home network
β
ββββββββββββββΌβββββββββββββ Cloudflare Tunnel, tailscale serve
β one address, e.g. β or port 443 with Caddy's certificate
β cloud.example.com β
ββββββββββββββ¬βββββββββββββ
β
ββββββββββββββΌβββββββββββββ ββββββββββββββββββββ
β gateway (Caddy) ββββββββΆβ auth β
β routes by path β check β passkeys Β· TOTP β
ββββββββββββββ¬βββββββββββββsessionβ sessions Β· admin β
β ββββββββββββββββββββ
ββββββββββββ¬βββββββββββ¬ββββββ΄ββββββ¬ββββββββββββ¬ββββββββββββ¬βββββββββββ
βΌ βΌ βΌ βΌ βΌ βΌ βΌ
login accounts cloud store photos share tphome
/login /accounts / /store /photos /share /home
ββββββββββββββββββββββββ this repo βββββββββββββββββββββββββββ β its own β
| Path | Service | Login required |
|---|---|---|
/login |
Sign-in page | β |
/ |
Dashboard | β |
/accounts |
Account management | β |
/store |
Store file browser, API at /store/api |
β |
/share |
Public share links (only the share endpoints of the Store API) | β |
/photos |
Gallery of the photos backed up from the iPhone (data from /store/api) |
β |
/home |
Home control panel, API at /home/api from tphome |
β |
/auth |
The auth API every client signs in against | β |
The gateway strips each module's prefix, so the services behind it still see the /api/... paths they always had. On a server everything above runs as plain processes under one systemd service: the services listen only on 127.0.0.1, and the gateway is the only way in (with Home, the switches also reach the MQTT broker on the local network).
Opening a protected service without being signed in:
Browser opens /store/photos
β
βΌ
Caddy asks auth: is this session cookie valid? (forward_auth)
β
βββ no βββΆ redirect to /login (/store/photos kept in a short-lived cookie)
β β
β βΌ
β passkey, password + TOTP code, or a QR code approved elsewhere
β β
β βΌ
β auth sets a host-only __Host- session cookie
β β
β βΌ
β back to /store/photos (only ever a path on this host)
β
βββ yes ββΆ request goes through to the service
- Passkeys β WebAuthn with Face ID, Touch ID or a security key, registered from
accounts. The default on the web, and no email needed: the browser offers the passkey it has for the site, also as autofill on the email field. - Password + TOTP β bcrypt-hashed password, plus a 6-digit code from an authenticator app once 2FA is set up. This is what the native app uses.
- QR code β the login page shows a code that a signed-in phone scans; the phone sees which device asks and from where, and approves or rejects it. Only the browser that showed the code can collect the session, and each code lasts two minutes.
Every session is stored server-side with the device (parsed from the user agent), the IP it signed in from and the one it was last used from (and, if TPCLOUD_GEOIP=1, their city and country), when it was last used, and how it signed in (for a QR code, which device approved it), so accounts can show where I'm signed in and revoke any of them, or all but this one. Sessions are revoked by their id; the session token itself never goes back to the browser.
Sessions slide forward while they're used: with "keep me signed in" (asked right after signing in, on by default) a browser stays signed in 30 days after it was last used; without it, until the browser closes or 12 hours without use. The native app gets 90 days. Every sign-in, failed attempt and revoked session is also kept for six months as the account's activity, with device, place and IP.
- One host, one cookie β the session cookie is
__Host-prefixed: Secure, HttpOnly, SameSite=Lax, host-only, so no other site or subdomain can read, set or overwrite it. - Limited attempts β 5 wrong passwords per account (or 20 per address) in 15 minutes lock sign-in for a while; a half-signed-in session is thrown away after 5 wrong TOTP codes, and each code works once.
- Nothing to enumerate β unknown emails get the same answers, in the same time, as real ones.
- Strict headers β HSTS, a Content-Security-Policy that only allows this host's own scripts,
nosniff, no framing by other sites; files users upload are served sandboxed, so an uploaded HTML page can't run scripts. - Little exposed β every service listens only on
127.0.0.1; the gateway listens only on what the chosen way in needs (and, with Home, the MQTT broker and/firmwareon the local network for the switches); the APIs have no docs or schema online.
The owner account is created by the guided setup. From there, admins can create users, change roles and revoke anyone's sessions from accounts, and services like Store check the is_admin flag to unlock admin-only features.
| Layer | Technology |
|---|---|
| Auth | FastAPI Β· Python Β· SQLModel Β· SQLite |
| Security | WebAuthn (webauthn) Β· TOTP (pyotp) Β· bcrypt |
| Storage | FastAPI Β· SQLModel Β· Pillow Β· ffmpeg |
| Frontends | Vue 3 Β· Vite, built into the tpcloud file and served by Caddy |
| App | Swift Β· SwiftUI Β· Swift Package Manager Β· XcodeGen |
| Gateway | Caddy Β· forward_auth |
| Exposure | Cloudflare Tunnel (cloudflared) Β· Tailscale Β· Caddy's own TLS |
| Server | One static Rust binary (cli/) run by systemd, supervising every process Β· Docker Compose as an alternative |
| Releases | GitHub Actions (arm64 and x86-64 builds) Β· GitHub Releases |
| Site | Cloudflare Workers static assets |
On a Linux machine (a Raspberry Pi, an old PC, a VPS; arm64 or x86-64):
curl -fsSL https://tpcloud.migueltaibo.com/install | shThat downloads the right file for the machine and runs sudo tpcloud install. The file can also be downloaded by hand from tpcloud.migueltaibo.com or the releases, then sudo ./tpcloud-linux-arm64 install.
It's a single static binary (~75 MB) and needs nothing else: no Docker, no Python, nothing from the package manager. Inside it carries its own Python with every service, the built web apps, tphome's API, and static builds of Caddy, Mosquitto, ffmpeg, rsync and cloudflared. Installing unpacks them to /opt/tpcloud, copies itself to /usr/local/bin and adds a systemd service, tpcloud serve, that runs each process and restarts it if it stops. The guided setup asks where Store keeps files, which modules to turn on, how it'll be reached (only at home, Tailscale, a Cloudflare Tunnel or your own domain on port 443) and creates your admin account.
Settings, databases and caches live in /var/lib/tpcloud (tpcloud.env, data/, home/, gateway-extra/, logs/) and an update never touches them. sudo tpcloud update gets the latest release from GitHub (checked against its SHA-256) and installs it; install run from an older file does the same, unless it's given --this-file. The service looks for a new release once a day, and tpcloud status says when there is one. install --adopt <folder> takes over a server that ran with Docker from tpcloud and tphome clones in that folder, copying their settings and data, with about a second down.
Backups. Every night the service copies TPCloud's own data (settings, every database: accounts, the photo index, albums, share links, the house; Home's firmware) to the folder set by the guided setup or sudo tpcloud backup --to <folder>, best on another disk. They're a few MB each, made with SQLite's online backup while everything keeps running, and the last 14 are kept. Your files aren't in them (they're on their own disk, with Store's own mirror to a backup disk), nor are thumbnails and previews, which are made again. If the folder is inside that mirror's destination, the mirror leaves it alone. sudo tpcloud restore <backup> puts one back, after backing up what's there.
| Command | What it does |
|---|---|
tpcloud |
A terminal app: status, Home, accounts, modules, logs and setup |
tpcloud status |
What's running, the address, the modules and where files and data are |
tpcloud setup |
The guided configuration again |
tpcloud update [--force] |
Get the latest release from GitHub and install it |
tpcloud backup [--to <folder>] Β· --list Β· --off |
Back up now (and set where), list the backups, or stop the nightly ones |
tpcloud restore <backup> |
Put a backup's settings and databases back |
tpcloud start Β· stop Β· restart |
Start everything, stop it (data is kept) or restart it |
tpcloud logs [process] [-f] |
A process's log, or every one's |
tpcloud module list Β· enable home Β· disable home |
Turn Store or Home on and off |
tpcloud users Β· users add <email> [--admin] |
List the accounts or add one |
tphome |
Every blind, its position and whether it's connected (there while Home is on) |
tphome --blind B0301 up |
Raise a blind; also down, stop, or a position from 0 (closed) to 100 (open) |
tphome talks to home-api on its local port from the server itself, so it needs no session: whoever can run commands there already owns the server.
To build it: cli/build.sh writes cli/dist/tpcloud-linux-<arch>. It needs Docker on the building machine only (the runtime and the Rust binary are both built in containers) and tphome cloned next to this folder. Pushing a v* tag builds both architectures on GitHub Actions and publishes them as a release, which is what the site's download links point to.
The same services also run as Docker containers, if you'd rather manage them that way. The installer script asks the same questions:
curl -fsSL https://raw.githubusercontent.com/mtaibo/tpcloud/main/install.sh | bashIt asks whether you want Store and tphome, where Store keeps your files and how you'll reach it (only at home, Tailscale, or your own domain through a Cloudflare Tunnel or an open port), creates your admin account, installs Docker if it's missing and starts everything. Run it again to change any of that; ./install.sh --update gets the latest version of everything and restarts it without asking anything, and --dry-run shows what it would do without changing anything.
By hand:
git clone https://github.com/mtaibo/tpcloud
cd tpcloud
cp .env.example .env # address, how it's reached, modules, where Store keeps files
docker compose up -d --build
OWNER_EMAIL=you@example.com OWNER_PASSWORD=... docker compose run --rm -e OWNER_EMAIL -e OWNER_PASSWORD auth python scripts/bootstrap.pyStore runs behind the store profile and Home's control panel behind home (COMPOSE_PROFILES=store,home, plus tunnel for a Cloudflare Tunnel). tphome, if you want it, is cloned next to this folder and started after it, since tpcloud creates the tpcloud_gateway network its API joins.
Beyond localhost the gateway only publishes what .env asks for: port 443 for your own domain or the home network, and, where tphome is installed, port 80 on the local network for nothing but /firmware, so its switches can download OTA updates.