readysite / CLAUDE.md
11.6 KB
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

ReadySite is an open-source platform that makes it easy for anyone to build, launch, and manage websites. Inspired by WordPress, PocketBase, and n8n, ReadySite is built for a world where AI lets anyone create software — the remaining hurdle is getting it live. ReadySite eliminates the DevOps barrier so you can go from idea to running site without managing servers, containers, or infrastructure. Built with Go for speed and reliability.

Ready, Site, Go!

Commands

# Run tests
go test ./...                                        # All tests
go test -v ./pkg/database/...                        # Verbose, single package
go test -run TestCollection ./...                    # Single test by name
go test -v -run TestCollection/Insert ./pkg/database # Subtest

# Run applications locally
go run ./example                        # Example app on :5000
go run ./website                        # Website CMS on :5000

# Deploy to cloud
go run ./cmd/launch                     # Deploy all servers
go run ./cmd/launch --server app-1      # Deploy specific server
go run ./cmd/launch --new example       # Create new server

# Connect to servers
go run ./cmd/connect                    # SSH (auto-selects single server)
go run ./cmd/connect docker ps          # Run command on server

# Publish website image to registry
go run ./cmd/publish --skip-dockerhub --build-server website-2  # Build on website-2, push to registry
go run ./cmd/publish --skip-dockerhub                           # Push existing local image to registry
go run ./cmd/publish                                            # Push to both registry and Docker Hub
go run ./cmd/publish --tag v1.0.0                               # With specific tag

Architecture

Five composable layers:

Layer Package Purpose
Database pkg/database Type-safe SQLite CRUD with Collection[E]
Application pkg/application MVC web framework with HTMX/HATEOAS
Frontend pkg/frontend Interactive JavaScript islands (React)
Platform pkg/platform Cloud deployment (DO, AWS, GCP)
Assistant pkg/assistant AI provider abstraction (OpenAI, Anthropic)

Key Conventions

  • IDs are ALWAYS strings (UUIDs, never integers)
  • SQL uses PascalCase - WHERE UserID = ? not user_id
  • Templates by filename - {{template "card.html" .}} not path
  • Controller access - {{home.Method}} not {{.home.Method}}
  • Error handling - Use c.RenderError(w, r, err) for HTMX-compatible errors
  • Zero values - Use cmp.Or(value, default) for defaults

Controller Pattern

// Factory returns (name, controller) tuple
func Home() (string, *HomeController) {
    return "home", &HomeController{}
}

type HomeController struct {
    application.BaseController
}

func (c *HomeController) Setup(app *application.App) {
    c.BaseController.Setup(app)
    http.Handle("GET /", app.Serve("index.html", nil))
    http.Handle("GET /api/data", app.Method(c, "GetData", nil))
    http.Handle("POST /api/data", app.Method(c, "Create", RequireAuth))  // With bouncer
}

// Handle - VALUE receiver for request isolation (critical!)
func (c HomeController) Handle(r *http.Request) application.Controller {
    c.Request = r
    return &c
}

// Public methods accessible in templates as {{home.Message}}
func (c *HomeController) Message() string {
    return "Hello, World!"
}

Bouncers (Route Guards)

func RequireAuth(app *application.App, w http.ResponseWriter, r *http.Request) bool {
    if GetCurrentUser(r) == nil {
        http.Redirect(w, r, "/signin", http.StatusSeeOther)
        return false
    }
    return true
}

// Usage in Setup()
http.Handle("GET /admin", app.Serve("admin.html", RequireAuth))
http.Handle("POST /delete", app.Method(c, "Delete", RequireAdmin))

Model Pattern

Models use package-level initialization:

// models/db.go
var (
    DB    = engines.NewAuto()  // Selects engine based on DB_URL/DB_PATH env vars
    Posts = database.Manage(DB, new(Post))
)

// models/post.go
type Post struct {
    database.Model
    Title   string
    Content string
}

Collection Methods

Method Returns Description
Get(id) (*E, error) Find by ID
First(where, args...) (*E, error) Single result
Search(where, args...) ([]*E, error) Multiple results
All() ([]*E, error) All records
Insert(entity) (string, error) Create (generates UUID, returns ID)
Update(entity) error Save changes
Delete(entity) error Remove

SQL Injection Prevention (CRITICAL)

Always use parameterized queries:

// CORRECT - parameterized (safe)
Posts.Search("WHERE UserID = ? AND Status = ?", userID, status)

// WRONG - string concatenation (SQL injection!)
Posts.Search("WHERE Name = '" + userInput + "'")  // NEVER DO THIS

Template Usage

<!-- views/layouts/base.html -->
<!DOCTYPE html>
<html>
<body>{{block "content" .}}{{end}}</body>
</html>

<!-- views/index.html -->
{{template "base.html" .}}
{{define "content"}}
<h1>{{home.Message}}</h1>
{{range $post := home.Posts}}
    <div>{{$post.Title}}</div>
{{else}}
    <p>No posts yet.</p>
{{end}}
{{end}}

Template directories:

  • views/layouts/ - Pre-loaded at startup
  • views/partials/ - Pre-loaded, use {{template "card.html" .}}
  • views/static/ - Served as static files
  • views/**/*.html - Loaded on-demand

RenderError Returns 200 OK

RenderError returns HTTP 200 intentionally - required for HTMX to swap error HTML into target elements.

func (c *SiteController) Create(w http.ResponseWriter, r *http.Request) {
    site, err := createSite(r)
    if err != nil {
        log.Printf("[sites] Create failed: %v", err)  // Always log
        c.RenderError(w, r, err)                       // Returns 200 with error HTML
        return
    }
    c.Redirect(w, r, "/sites/"+site.ID)
}

SSE Streaming

func (c *HomeController) Live(w http.ResponseWriter, r *http.Request) {
    stream := c.Stream(w)
    for {
        select {
        case <-r.Context().Done():
            return
        case msg := <-updates:
            stream.Render("messages", "partials/message.html", msg)
        }
    }
}
<div hx-ext="sse" sse-connect="/api/live" sse-swap="messages">Loading...</div>

Interactive Islands

For rich components (editors, charts), use React islands:

application.Serve(views,
    frontend.WithBundler(&esbuild.Config{
        Entry:   "index.ts",
        Include: []string{"components"},
    }),
)
{{render "Counter" (dict "initial" 5)}}

Use HTMX for forms/navigation, React islands for complex interactivity.

Platform & Deployment

p, err := digitalocean.New(token)
server, err := p.CreateServer(platform.ServerOptions{
    Name:   "app-1",
    Size:   platform.Small,
    Region: platform.SFO,
})
server.WaitForSSH(5 * time.Minute)
server.Copy("./build/app", "/usr/local/bin/app")

infra.json

{
  "platform": {"provider": "digitalocean", "token": "$DIGITAL_OCEAN_API_KEY"},
  "servers": {"app": {"size": "small", "binaries": ["example"]}},
  "binaries": {"example": {"source": "./example", "ports": [{"host": 80, "container": 8080}]}}
}

Each binary needs a Dockerfile in its source directory.

Assistant Layer

AI provider abstraction for OpenAI and Anthropic:

import "github.com/readysite/readysite/pkg/assistant/providers/openai"

client := openai.New(apiKey)
response, err := client.Chat(ctx, []assistant.Message{
    {Role: "user", Content: "Hello"},
})

Providers: mock (testing), openai, anthropic

Environment Variables

Variable Required Description
PORT No Server port (default: 5000)
ENV No "production" disables HMR, sanitizes errors
AUTH_SECRET Production JWT signing secret (random if unset, fatals in production)
DB_URL No Remote libSQL server URL (e.g., libsql://mydb.turso.io)
DB_TOKEN No Authentication token for remote libSQL server
DB_PATH No Local database file path (e.g., /data/app.db)
TLS_CERT No Path to TLS certificate file (enables HTTPS on :443)
TLS_KEY No Path to TLS private key file (required with TLS_CERT)
RESEND_API_KEY No Resend email API key (falls back to log-only emailer)
ADMIN_EMAIL No Auto-create admin user on first run (website CMS)
ADMIN_NAME No Admin display name (defaults to email prefix)
SITE_NAME No Site name for CMS seeding
SITE_DESC No Site description for CMS seeding
HOSTING_URL No Redirect URL when website setup is incomplete
REGISTRY_HOST No Docker registry hostname (default: registry:5000)
STRIPE_API_KEY No Stripe secret key (sk_test_... or sk_live_...). Enables Stripe Checkout.
STRIPE_WEBHOOK_SECRET No Stripe webhook endpoint signing secret (whsec_...)
STRIPE_HOBBY_PRICE_ID No Stripe Price ID for Hobby plan (price_...)
STRIPE_PRO_PRICE_ID No Stripe Price ID for Pro plan (price_...)
DIGITAL_OCEAN_API_KEY Deploy DigitalOcean API token
DIGITAL_OCEAN_PROJECT Deploy DigitalOcean project UUID

Database Selection: engines.NewAuto() selects based on environment:

  • DB_URL + DB_TOKEN → Remote replica (Turso/libSQL)
  • DB_PATH only → Local file
  • Neither → In-memory

Documentation

Developer docs (in this repo):

  • docs/database.md - Collections, queries, engines
  • docs/application.md - Controllers, templates, SSE, bouncers
  • docs/frontend.md - Interactive islands, bundler
  • docs/platform.md - Servers, deployment, infra.json
  • docs/assistant.md - AI providers, tool calling, streaming
  • docs/hosting.md - Hosting app architecture
  • docs/deployment.md - infra.json, Dockerfiles, environment setup
  • docs/security.md - Auth, rate limiting, input validation
  • docs/testing.md - Database, controller, AI testing patterns
  • docs/examples.md - Recipes and code patterns
  • docs/troubleshooting.md - Common issues and solutions
  • docs/architecture.md - System design and layer composition
  • docs/changelog.md - Version history and migration notes
  • website/CLAUDE.md - AI-native CMS specific patterns

User-facing docs (readysite.org website):

  • readysite.org/views/docs/docker.html - Getting Started guide
  • readysite.org/views/docs/api.html - API Reference
  • readysite.org/views/docs/collections.html - Collections guide

IMPORTANT: When adding features or changing configuration, update BOTH the developer docs (docs/*.md) AND the user-facing docs (readysite.org/views/docs/*.html). The .org docs are what users see at readysite.org.

Common Pitfalls

  1. Value vs Pointer Receivers - Handle() uses value receiver for request isolation; all other methods use pointer receivers
  2. Template Access - Use {{home.Method}} not {{.home.Method}} (controllers are functions, not data)
  3. Controller Order - Register catch-all routes last (e.g., /{handle} profile routes)
  4. CGO and libsql - Database requires CGO; use golang:1.24-bookworm for builds, debian:bookworm-slim for runtime
  5. Provider Errors - All provider New() functions return (*Platform, error)
  6. Package-level Models - Initialize DB and collections as package vars, not in init() or main()
  7. Collection Search - First argument is WHERE clause: Posts.Search("WHERE Published = ?", true) not just "Published = ?"
  8. SQL Injection - Always use ? placeholders, never concatenate user input into SQL
← Back