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.
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.
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.
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.
The Kimbia community PDS. Create your first atproto account here, or migrate an existing one without losing your handle, posts, or followers.
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
└────────────────┘
PDS_BLOBSTORE_S3_* env vars. Blobs = images, FIT files, share cards.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.
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.
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.
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.
ufw allow 22,80,443. Install fail2ban. Disable root SSH. Install Docker + Docker Compose. Takes 10 minutes.
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.
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.
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.
Create a test account. Sign in from the official Bluesky client using yourhandle.kimbia.social. Check bsky-debug.app/handle - all green.
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.
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.
pdsadmin create-invite-code → returns one-use code. Add --use-count 10 for a multi-use.pdsadmin account create alice@example.com alice.kimbia.social. Or let them sign up with an invite code.pdsadmin account listpdsadmin account reset-password {did}. Sends a reset or prints a new password.pdsadmin account takedown {did}. Reversible. Use the atproto ozone stack later if we need real moderation.docker compose pull && docker compose up -d. Pin a version if you don't want silent updates.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.
/pds directory (SQLite + local blobs if any)pds.env + both private keysufw enable.PasswordAuthentication no in sshd_config.pds.env, change with pdsadmin.apt install unattended-upgrades for kernel/security only.Self-hosted on the same box (Docker, 5-min setup). Monitors https://kimbia.social/xrpc/_health. Alerts via Bluesky DM (or email, Telegram).
Built-in CPU/RAM/disk/network graphs. Free. Good enough for the first year.
Skip for now. Add if we cross ~1k accounts or get performance questions we can't answer from Hetzner graphs.
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.
alice.kimbia.social. Existing atproto users keep whatever they already have (.bsky.social, .npmx.social, their own). We do not force migration.