Atproto · working doc

Kimbia runs on atproto.

One identity, portable by design. Your handle, your posts, your activities - all yours, movable, yours to delete. This page is a working doc for how we run kimbia.social (our own PDS) and how anyone with an existing atproto account can sign in without creating a new one.

01. Why atproto

  • One identity. No separate Kimbia login. Your atproto account is who you are here.
  • Portable by design. If Kimbia disappears, your data is still on your PDS. You can export or move to another PDS in minutes.
  • Records, not rows. Activities, coaching windows, share cards - each is an atproto record in a public lexicon. Other apps can read and write the same data if you let them.
  • Social is already solved. Bluesky is the social layer; Kimbia just posts share cards there. We don't rebuild timelines or DMs.

02. Login flow · 3 mockups

All three are handle-only. No email field, ever. The atproto handle is the identity. Newcomers claim one on kimbia.social; existing atproto users sign in with whatever handle they already have. Difference is how much UI we put between the user and their handle. Click each tab and try it.

✓ Variant B picked. A and C kept as reference.

Welcome to Kimbia
Your running journal.
.kimbia.social
or
✓ Chosen

Variant B · Big claim · small signin

Primary action is the handle picker with .kimbia.social suffix baked in. Behind it, one smart rule: Continue = create if free, sign in if taken. Users don't have to know which they are.

  • + One primary path for the common case (claiming / returning to a kimbia.social handle). Zero decision for the user.
  • + Shows the kimbia.social suffix as a first-class design element - brand moment every signup.
  • + Secondary path ("I have a handle on another PDS") is honest about what it is - anyone on bsky.social, npmx.social, their own PDS.
  • + Safe to resolve-then-decide: atproto handles are public, so checking existence leaks nothing. PDS still enforces the password on signin.
  • Non-kimbia.social users need the second path - one extra click for them.

Feels: conversion-first, brand-heavy, friendly to both new and returning users.

All three call the same backend: new handle → create account on kimbia.social; existing handle → atproto OAuth via @atproto/oauth-client-browser. Never an email field.

03. kimbia.social

The Kimbia community PDS. Create your first atproto account here, or migrate an existing one without losing your handle, posts, or followers.

→ Migrate with PDS MOOver
Server details
Location
TBD · Hetzner Nuremberg or Falkenstein (EU)
Infrastructure
Hetzner Cloud · self-managed
Privacy
Subject to EU Data Protection laws (GDPR)
Signup
Open - anyone from kimbia.app, no invite code

04. Architecture

    Browser / Kimbia app
            │
            │  OAuth  (@atproto/oauth-client-browser)
            ▼
  ┌─────────────────────────────────────────┐
  │  kimbia.social  (DNS A → Hetzner IP)    │
  │  *.kimbia.social  (wildcard)            │
  └────────────────────┬────────────────────┘
                       │  :443 TLS
                       ▼
              ┌────────────────┐
              │   Caddy        │  auto-TLS (Let's Encrypt)
              └────────┬───────┘
                       │  :3000
                       ▼
              ┌────────────────┐       ┌───────────────┐
              │   PDS (Node)   │──────▶│  SQLite       │  /pds
              │   com.atproto  │       │  repo + acct  │
              └────────┬───────┘       └───────────────┘
                       │
                       ▼
              ┌────────────────┐
              │  S3 blobstore  │  FIT files, images, share cards
              └────────────────┘
kimbia.social (DNS A record)
Points to the Hetzner server. Wildcard *.kimbia.social also points here so handles like alice.kimbia.social resolve.
Caddy (auto-TLS)
Ships with installer.sh. Provisions Let's Encrypt certs for the root + wildcard. Fronts the PDS on :443.
PDS (Docker)
Node service. SQLite for repo/account data. Exposes XRPC endpoints (com.atproto.*) and OAuth authz.
Blob store (S3)
Hetzner Object Storage (same region as the VPS - keeps egress free). PDS_BLOBSTORE_S3_* env vars. Blobs = images, FIT files, share cards.
Kimbia app → PDS
Kimbia's Svelte app talks to the user's PDS via atproto OAuth. Records (activities, coaching windows) live in the user's repo, not our DB.
Firehose → Kimbia indexer
Optional later: Kimbia subscribes to com.atproto.sync.subscribeRepos to index Kimbia records from anyone's PDS (not just kimbia.social).

05. Self-host steps (Hetzner)

No Cloudflare tunnel. Hetzner gives a public IP, Caddy handles TLS, the installer does the rest. End-to-end: about an hour including DNS propagation.

01

Buy both domains

kimbia.app (already owned - the app) + kimbia.social (new - the PDS, ~€15/yr). Why two: wildcard *.kimbia.social DNS points at the PDS, so user handles are alice.kimbia.social. If we wildcarded kimbia.app instead, we would lose every other subdomain (api, docs, status…). Registrar with 2FA. Cloudflare DNS (no proxy) for both.

02

Hetzner VPS (plain, not Coolify)

Dedicated box for the PDS, separate from the Coolify box that runs the Kimbia app. Why: PDS is an appliance - installer.sh ships its own Caddy + Watchtower and wants to own the machine. Running it under Coolify means a proxy fight (Coolify's Traefik vs Caddy) and Coolify trying to manage a compose file it didn't write. Separation is also a blast-radius win: if the app crashes, user PDSes keep working. CPX21 (3 vCPU, 4GB RAM, 80GB SSD, €7.50/mo), Ubuntu 24.04 LTS, SSH key only. Add a Storage Box (1TB, €3.20/mo) in the same region for backups.

03

DNS records

On kimbia.social: A record @ → PDS server IP, A record * → PDS server IP (wildcard, for user handles). On kimbia.app: nothing changes - it keeps pointing at the Coolify box.

04

Harden the box

ufw allow 22,80,443. Install fail2ban. Disable root SSH. Install Docker + Docker Compose. Takes 10 minutes.

05

Run the installer

curl https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh | sudo bash. Prompts for domain (kimbia.social) and admin email. Installer creates /pds, writes pds.env, starts Caddy + PDS + Watchtower via docker-compose. Caddy provisions TLS on first request.

06

Move blobs to Hetzner Object Storage

Create a bucket in the same region as the VPS (free egress between them). Edit /pds/pds.env: remove PDS_BLOBSTORE_DISK_LOCATION, add PDS_BLOBSTORE_S3_BUCKET, _REGION, _ENDPOINT, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY, _FORCE_PATH_STYLE=false. docker compose down && docker compose up -d.

07

Open signups (no invite code)

Anyone who lands on kimbia.app should be able to claim a handle in one click - no invite gate. Edit /pds/pds.env, set PDS_INVITE_REQUIRED=false, restart the stack. The admin password stays admin-only; signup is public. Spam/abuse: re-enable invites if it becomes a problem, or add a Turnstile challenge to the Kimbia signup UI.

08

Verify

Create a test account. Sign in from the official Bluesky client using yourhandle.kimbia.social. Check bsky-debug.app/handle - all green.

09

Back up the two keys

Copy PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX and PDS_RECOVERY_DID_KEY from pds.env into 1Password AND an encrypted USB. These are irreplaceable - losing them means users lose account recovery.

06. Admin & invites

pdsadmin ships with the installer. Admin password lives in /pds/pds.env - keep it in 1Password, rotate every 90 days. For heavier ops use the goat CLI.

Create an invite code
pdsadmin create-invite-code → returns one-use code. Add --use-count 10 for a multi-use.
Create an account (for a friend)
pdsadmin account create alice@example.com alice.kimbia.social. Or let them sign up with an invite code.
List accounts
pdsadmin account list
Change a password
pdsadmin account reset-password {did}. Sends a reset or prints a new password.
Takedown (moderation)
pdsadmin account takedown {did}. Reversible. Use the atproto ozone stack later if we need real moderation.
Update PDS
Watchtower auto-pulls. To force: docker compose pull && docker compose up -d. Pin a version if you don't want silent updates.

07. Backups & keys

▲ Two keys that must never be lost
  • PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX - used by the PDS to perform PLC operations (handle changes, account recovery).
  • PDS_RECOVERY_DID_KEY - added to every user's DID. Last-resort recovery if everything else is lost.

Store both offline (encrypted USB in a drawer) AND in 1Password. Never just on the server.

Daily · automated
What: Full /pds directory (SQLite + local blobs if any)
Where: Hetzner Storage Box, via <code>restic</code> cron job
Point-in-time recovery. Encrypted at rest.
Continuous · optional
What: SQLite stream via Litestream
Where: Same Storage Box or S3
Recover to any second, not just last night. Set <code>PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT=true</code>.
Monthly · manual
What: Full pds.env + both private keys
Where: 1Password + encrypted USB in a drawer
If the server is gone AND the Storage Box is gone, the keys still let us rebuild identity for users.
Live · provider
What: Blobs on S3
Where: Hetzner Object Storage (replicated by provider)
We trust the provider for 99.9%. Backup only if paranoid.

08. Security checklist

  • UFW firewall - Allow 22, 80, 443 only. ufw enable.
  • SSH key-only - No password auth. PasswordAuthentication no in sshd_config.
  • fail2ban - Default jails for SSH. Adds friction to brute force.
  • 2FA - Hetzner account, domain registrar, email. Non-negotiable.
  • Admin password rotation - Every 90 days. It's in pds.env, change with pdsadmin.
  • Key separation - The PLC rotation key and the recovery key should never live only on the server. Offline copies always.
  • Unattended upgrades - apt install unattended-upgrades for kernel/security only.

09. Dashboards

Uptime Kuma

Self-hosted on the same box (Docker, 5-min setup). Monitors https://kimbia.social/xrpc/_health. Alerts via Bluesky DM (or email, Telegram).

Hetzner Cloud Console

Built-in CPU/RAM/disk/network graphs. Free. Good enough for the first year.

Grafana + Prometheus

Skip for now. Add if we cross ~1k accounts or get performance questions we can't answer from Hetzner graphs.

PDS MOOver

Bailey's service. Not a dashboard but a safety net: backs up user repos independently of us, helps users migrate off us if they want. Opt-in for our users.

10. Decisions

Signup gate?
✓ Decided → Open. Anyone from kimbia.app creates an account in one click, no invite code. Revisit only if spam/abuse forces our hand.
Handle shape?
✓ Decided → Both domains. New users land on alice.kimbia.social. Existing atproto users keep whatever they already have (.bsky.social, .npmx.social, their own). We do not force migration.
Do we force new users onto kimbia.social?
✓ Decided → No. Almost transparent: newcomers sign up without knowing the word "atproto"; existing atproto users sign in with their current handle. Both paths land in the same app with zero friction.
Blob storage?
✓ Decided → Hetzner Object Storage, same region as the PDS VPS. Free egress between them, EU-hosted, provider-replicated.
Run our own relay + appview?
✓ Decided → No. PDS only. Relay + appview = a different beast (terabytes, firehose subscription, moderation pipeline). Revisit in No 03 · Long Run if at all.
Kimbia atproto · working doc · not a commitment yet