Hosting App
Architecture and internals of the ReadySite hosting application (hosting/).
Overview
The hosting app manages the lifecycle of ReadySite websites. Users sign in with email, create sites, and manage them through a React Flow dashboard. Each site runs as a Docker container with a Caddy reverse proxy handling TLS.
Architecture
hosting/
├── main.go # Entry point
├── controllers/
│ ├── home.go # Landing page, pricing, about
│ ├── auth.go # Magic link authentication
│ ├── billing.go # Plan management
│ ├── settings.go # User settings
│ └── sites.go # Site CRUD + API
├── models/
│ ├── db.go # Database setup
│ ├── user.go # User model
│ ├── site.go # Site model
│ └── auth_token.go # Magic link tokens
├── internal/
│ ├── access/
│ │ ├── auth.go # JWT sessions
│ │ └── limiter.go # Rate limiting
│ ├── websites/
│ │ ├── docker.go # Docker commands
│ │ └── launch.go # Site provisioning
│ └── payments/
│ └── payments.go # Billing logic
├── frontend/
│ └── components/
│ └── SitesPage.jsx # React Flow dashboard
└── views/
Authentication (Magic Link)
The hosting app uses passwordless email authentication:
Flow
- User enters email at
/auth/signin - Server generates a 32-byte random token
- Token is SHA-256 hashed and stored with 10-minute expiration
- Plaintext token is emailed as a magic link
- User clicks link →
/auth/verify?token=... - Server atomically claims the token (prevents race conditions)
- User record is created (if new) and session cookie is set
Token Security
| Property | Value |
|---|---|
| Generation | 32 bytes from crypto/rand |
| Storage | SHA-256 hash (plaintext never stored) |
| Expiration | 10 minutes |
| Single-use | Atomic UPDATE ensures one-time use |
| Cleanup | Hourly deletion of used/expired tokens |
Rate Limiting
Magic link requests are rate-limited to 3 per minute per IP to prevent email spam.
Models
User
type User struct {
database.Model // ID, CreatedAt, UpdatedAt
Email string // Unique
Name string // Optional
Verified bool // Set on first magic link verification
}
user.Sites() // []*Site - all user's sites
Site
type Site struct {
database.Model
UserID string
Name string
Plan string // "free" or "pro"
Status string // launching, active, failed, shutdown, deleted
Description string
AuthSecret string // Per-site JWT signing key
}
site.IsPro() bool
site.Subdomain() string // Same as site.ID
site.Owner() (*User, error)
AuthToken
type AuthToken struct {
database.Model
TokenHash string // SHA-256 hex
Email string
ExpiresAt time.Time
Used bool
}
Site Lifecycle
Create → Launching → Active
↘ Failed
Active → Shutdown (user stops)
Shutdown → Active (Pro: restart)
→ Deleted (Free: permanent delete)
Create Site
- Generate slug from name (lowercase, hyphens, max 30 chars)
- Check uniqueness (auto-increment suffix if needed)
- Generate per-site AUTH_SECRET
- Insert with
Status: "launching" - Async launch: provision container → configure Caddy → start
Launch Pipeline
Each step is idempotent (safe to retry):
- Provision —
docker runcreates the container - Configure — Writes Caddy config for
{id}.readysite.app - Start —
docker startactivates the container
Real-time progress is streamed to the dashboard via SSE events.
Admin Access
Users access their site's admin panel through a short-lived JWT:
- Hosting app generates a 5-minute JWT signed with the site's AuthSecret
- User is redirected to
https://{id}.readysite.app/auth/token?token={JWT} - The website container validates the JWT using the matching AuthSecret
Plans
| Plan | Features |
|---|---|
| Free | Container only, in-memory database, data lost on restart |
| Pro | Persistent volume at /data, database survives restarts |
Upgrade to Pro
- Create persistent volume at
/data/sites/{id} - Stop and remove existing container
- Recreate with
-v /data/sites/{id}:/data -e DB_PATH=/data/app.db - Update plan to "pro"
Docker Management
Container Configuration
Each site container runs the pre-built website image:
docker run -d \
--name=site-{id} \
--network=readysite \
--restart=unless-stopped \
-e ENV=production \
-e PORT=5000 \
-e ADMIN_EMAIL={user.email} \
-e AUTH_SECRET={site.authSecret} \
localhost:5001/website
Caddy Reverse Proxy
Each site gets a Caddy config file at /caddy/sites/{id}.caddy:
{id}.readysite.app {
reverse_proxy site-{id}:5000
}
Caddy handles automatic TLS via Let's Encrypt.
Data Size Tracking
Pro sites report their data usage via docker exec du -sb /data.
React Flow Dashboard
The site management UI is a React Flow canvas (frontend/components/SitesPage.jsx):
Features
- Custom SiteNode — Color-coded status badges, plan indicators, domain display
- Detail Panel — Right sidebar with site info and actions
- Grid Layout — Responsive: 1/2/3 columns based on viewport
- Position Persistence — Node positions saved to localStorage
- SSE Updates — Real-time status updates during launch/shutdown
- Launch Tour — 3-step modal guides new users through first site creation
- Confirmation Modals — Destructive actions require typing the site name
Node Status Colors
| Status | Color |
|---|---|
| Active | Green |
| Launching | Purple (animated) |
| Failed | Red |
| Shutdown | Gray (50% opacity) |
SSE Events
The dashboard connects to /api/sites/{id}/events for real-time updates:
const source = new EventSource(`/api/sites/${id}/events`)
source.addEventListener('status', (e) => {
const { status, message } = JSON.parse(e.data)
// Update site status in React state
})
Terminal statuses (active, failed, deleted) close the connection.
Environment Variables
| Variable | Required | Description |
|---|---|---|
AUTH_SECRET |
Yes (prod) | JWT signing for hosting sessions |
RESEND_API_KEY |
No | Email service for magic links (falls back to logger) |
DB_PATH |
No | Database file path |
DIGITAL_OCEAN_API_KEY |
No | For Pro site provisioning |