readysite / docs / hosting.md
6.7 KB
hosting.md

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

  1. User enters email at /auth/signin
  2. Server generates a 32-byte random token
  3. Token is SHA-256 hashed and stored with 10-minute expiration
  4. Plaintext token is emailed as a magic link
  5. User clicks link → /auth/verify?token=...
  6. Server atomically claims the token (prevents race conditions)
  7. 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

  1. Generate slug from name (lowercase, hyphens, max 30 chars)
  2. Check uniqueness (auto-increment suffix if needed)
  3. Generate per-site AUTH_SECRET
  4. Insert with Status: "launching"
  5. Async launch: provision container → configure Caddy → start

Launch Pipeline

Each step is idempotent (safe to retry):

  1. Provisiondocker run creates the container
  2. Configure — Writes Caddy config for {id}.readysite.app
  3. Startdocker start activates 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:

  1. Hosting app generates a 5-minute JWT signed with the site's AuthSecret
  2. User is redirected to https://{id}.readysite.app/auth/token?token={JWT}
  3. 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

  1. Create persistent volume at /data/sites/{id}
  2. Stop and remove existing container
  3. Recreate with -v /data/sites/{id}:/data -e DB_PATH=/data/app.db
  4. 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
← Back