Self-hosted Hotly for exactly one creator. Apache License 2.0. Guests buy a one-time note or a weekly Penpal subscription; the creator replies from an inbox, including optional image/video (free or paid unlock). /dome is the retained community. Payments go to the operator’s Stripe account.
Browsers land on /penpal. The public origin is Caddy (/ + /api), not :3000 or :8080.
You supply the machine, DNS, and accounts. This project does not create a VPS, a domain, Docker, or Firebase / Stripe / SendGrid. Operators build images from source. There are no published prebuilt images.
Production is a Linux virtual machine you can SSH into. Windows is not supported (not as a server, and local Docker Desktop on Windows is not a supported host). Try the stack locally on Linux or macOS.
If you build images on the box (the documented path): 2 vCPU and at least 4 GB RAM. Typical cloud images have no swap. A 2 GB machine is too tight for docker compose up --build. Put the VM in a region close to the creator and the guests you expect — every page and upload is a round trip to this host.
Enable the provider’s automatic backups or disk snapshots. That is not enough by itself: PostgreSQL and media live in Docker volumes on that disk. Still run docs/operations.md on a schedule and copy .backup/ off the VPS. Losing the machine without an off-box copy means losing conversations and uploads.
Any generic Docker-capable Linux VPS works. A DigitalOcean Basic 2 vCPU / 4 GB class machine is enough. Similar: Hetzner Cloud, Linode, Vultr, OVHcloud, AWS Lightsail, Google Compute Engine. You need a VM. Render, Railway, Fly, and other single-service PaaS are not this Compose stack.
On that machine you need both:
- Docker Engine (
docker inforeaches a running daemon) - the Compose v2 plugin (
docker compose version, with a space)
The old docker-compose (hyphen) binary is not enough. The installer will not install Docker or Compose for you.
On an Ubuntu/Debian VPS as root (same command we used; Engine plus Compose v2):
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker info
docker compose versionNot for macOS. Official packages: Install Docker Engine. macOS local: OrbStack or Docker Desktop.
A hostname at any registrar, with access to create A/AAAA records. A raw public IP is a smoke host, not the origin guests, Google, Stripe, or recovery mail should use.
Moving the zone to Cloudflare is recommended. Keep the record DNS only (grey cloud) until Caddy has a Let’s Encrypt certificate; orange-cloud proxy hides the origin from Let’s Encrypt.
Operator-owned Firebase (Google sign-in), Stripe, and SendGrid. The stack boots if those keys are empty; the matching pages then say the provider is not configured.
Bare minimum for a public VPS that builds images on the box: 2 vCPU and 4 GB RAM. Typical cloud images have no swap.
Idle Compose is a few hundred MiB. On a 4 GB Ubuntu host, docker compose up --build can peak around 2.7 GB used. A 2 GB machine is too tight for on-box Next.js + Go builds.
Disk needs room for the OS, images, PostgreSQL, and media. Media growth is the long-term limit, not RAM. Measured breakdown: docs/operations.md.
Linux or macOS with Docker Engine and Compose v2. Installer (fail-fast if Docker is missing; does not install Docker or Bun):
./scripts/install.sh --localSkip providers or Compose while testing: --skip-firebase --skip-stripe --skip-sendgrid --skip-up --yes. Below 2 vCPU / 4 GB Docker RAM the installer stops; --skip-spec overrides that. The "System (paste this in a GitHub issue)" block is for bug reports.
Or by hand:
cp .env.example .env
# Set CREATOR_EMAIL. Replace SESSION_SECRET, RECOVERY_TOKEN_SECRET, and Garage keys.
# Leave Firebase, Stripe, and SendGrid empty until you have those credentials.
docker compose up --buildOpen http://localhost. Health: GET /api/health.
Compose starts PostgreSQL 18, runs db/migrations before the API, then the Go API, Next.js, Garage (private S3), and Caddy.
| Path | Who |
|---|---|
/ |
Redirects to /penpal |
/penpal |
Public landing: creator, USD prices, Checkout, “Powered by Hotly.” |
/chat |
Paid guest conversation (HTTP-only cookie after Checkout or recovery) |
/dome, /dome/:promptSlug |
Community; #note-<id> is a stable note fragment |
/creator, /creator/inbox, /creator/dome, /creator/settings |
Configured Firebase email only |
/auth/firebase |
Google sign-in |
/auth/chat-recovery |
Guest recovery request and consume |
USD only. Defaults: one-time notes $5 / 250, $10 / 500, $15 / 1,000 characters (ONE_TIME_PRICE_CENTS, ONE_TIME_PRICE_500_CENTS, ONE_TIME_PRICE_1000_CENTS); Custom must be at least $1 above the 1,000-character price; weekly Penpal $15 / 2,000 characters (WEEKLY_PRICE_CENTS=1500). Named-tier Checkout ignores browser amount and currency.
apps/web— Next.js UI (Bun). Same-origin/apifetches with cookies. Favicon, apple-touch, android-chrome, wordmarks, andsite.webmanifestare inapps/web/public.apps/server— Go API (Chi, pgx, sqlc). Automated tests live here only.emails— React Email sources for the three SendGrid bodies.bun run emails:render, then rebuildserver. Details:emails/README.md.
There is no frontend test suite. UI is checked with bun run check-types / bun run build and by using the app.
On GitHub, pull requests, main, and v*.*.* tags run Go tests, bun run check-types, and docker compose config. Cutting a version tag publishes a GitHub Release after those checks pass.
Local apps without Compose (API still needs Postgres and the env):
bun run devBring DNS up before the first public Compose. A raw droplet IP is a smoke host, not the origin guests, Google, Stripe, or recovery mail should use.
Pin a Release tag (v0.1.5), not a drifting main. Hostname + A/AAAA first (Cloudflare: DNS only until the certificate exists). 2 vCPU / 4 GB if you build on the box. TCP 80 and 443. Docker Engine and Compose v2. Then curl — do not hand-edit .env or run Compose yourself:
curl -fsSL https://raw.githubusercontent.com/designAtHotly/hotly-os/v0.1.5/scripts/install.sh | bash -s -- --tag v0.1.5 --github designAtHotly/hotly-osThat unpacks to ~/hotly-os. Answer the prompts for https://your.domain.example (canonical origin, Open Graph, /robots.txt, /sitemap.xml). The installer writes .env, uses the prod overlay (Postgres / API / Next.js not published), and starts Compose. From a checkout of that tag: ./scripts/install.sh --local.
After that, in the provider consoles: Firebase authorized domain, Stripe webhook https://your.domain.example/api/webhooks/stripe (Checkout, subscription, and invoice events in docs/operations.md; paste whsec_ at the installer prompt), SendGrid API key starting SG. plus a verified sender (app.sendgrid.com).
Full sequence, backup/restore, upgrades, SendGrid vs Twilio keys: docs/operations.md.
Apache License 2.0. See LICENSE and NOTICE.
CONTRIBUTING.md— how to run checks and open a PRCODE_OF_CONDUCT.md— Contributor Covenant 2.1SECURITY.md— private vulnerability reports, not public issuesSUPPORT.md— operations vs GitHub vs provider consolesdocs/github.md— enable Private vulnerability reporting and Dependabot on this GitHub repository