Website: AI-Native CMS
An AI-native content management system where AI assistance is a first-class feature. Status: Work in Progress
Target Audience
ReadySite targets technology enthusiasts who want to build with AI - a WordPress-inspired platform for the AI era. While the long-term goal is accessibility for non-technical users, the current focus is on users who are comfortable with:
- Basic HTML/CSS knowledge (for page content)
- API key configuration (for AI providers)
- Technical concepts like "collections" and "endpoints"
This is explicitly not targeting drag-and-drop users (like Squarespace/Wix) today, though a visual editor is on the roadmap.
Overview
The website is a CMS built on ReadySite's framework layers. It demonstrates how to build a full application with:
- AI chat with tool execution
- Hierarchical pages with templates
- Dynamic collections with typed schemas
- Mutation tracking for undo/redo
Architecture
website/
├── main.go # Bootstrap, seed data, register controllers
├── Dockerfile # Multi-stage build
├── controllers/ # 9 controllers (2,330 LOC)
│ ├── auth.go # JWT authentication
│ ├── setup.go # First-run wizard
│ ├── dashboard.go # Admin home
│ ├── pages.go # Page CRUD
│ ├── collections.go # Collection + document CRUD
│ ├── endpoints.go # Serverless endpoints (WIP)
│ ├── chat.go # AI conversation + streaming
│ ├── workspace.go # Main admin workspace
│ └── site.go # Public page rendering
├── models/ # Data entities
│ ├── db.go # Database setup
│ ├── user.go # Users with roles
│ ├── page.go # Hierarchical pages
│ ├── collection.go # Collections + documents
│ ├── conversation.go # Chat + messages + mutations
│ ├── endpoint.go # Serverless endpoints
│ └── settings.go # Key-value settings
├── internal/
│ ├── access/ # Role constants, permission checking, ACL
│ ├── assist/ # AI conversation helpers
│ ├── render/ # Template functions for pages
│ ├── runtime/ # Endpoint execution (Lua sandbox)
│ ├── schema/ # Collection field types + validation
│ └── tools/ # AI tool definitions + executor
└── views/ # 28 templates (2,627 LOC)
Key Features
1. AI Chat with Tool Execution
The AI can create, update, and delete content through defined tools:
// 15 tools defined in internal/tools/
tools.Register("create_page", ...)
tools.Register("update_page", ...)
tools.Register("delete_page", ...)
tools.Register("create_collection", ...)
tools.Register("create_document", ...)
// ... etc
Chat streams responses via SSE, executing tools in real-time:
<div hx-ext="sse" sse-connect="/admin/chat/{{.ID}}/stream" sse-swap="content">
<!-- AI responses stream here -->
</div>
Creating Custom AI Tools
To add a new AI tool, follow these three steps:
Step 1: Define the tool schema (in internal/tools/)
Create a file for your tool or add to an existing file:
// internal/tools/notifications.go
package tools
import "github.com/readysite/readysite/pkg/assistant"
var sendNotificationTool = assistant.NewTool("send_notification", "Send a notification to users").
String("title", "Notification title", true). // required
String("message", "Notification body", true). // required
String("user_id", "Target user ID (optional, all users if empty)", false).
Build()
// Arguments struct for parsing
type SendNotificationArgs struct {
Title string `json:"title"`
Message string `json:"message"`
UserID string `json:"user_id"`
}
Available parameter types:
String(name, description, required)- Text parameterInt(name, description, required)- Integer parameterNumber(name, description, required)- Float parameterBool(name, description, required)- Boolean parameterObject(name, description, required)- JSON object parameterArray(name, description, required)- JSON array parameter
Step 2: Register the tool (in internal/tools/tools.go)
Add your tool to the All() function:
func All() []assistant.Tool {
return []assistant.Tool{
// ... existing tools ...
sendNotificationTool, // Add your tool
}
}
Step 3: Implement the executor (in internal/tools/executor.go)
Add a case to the Execute switch and implement the handler:
func (e *Executor) Execute(call assistant.ToolCall) (string, error) {
switch call.Name {
// ... existing cases ...
case "send_notification":
return e.sendNotification(call)
}
}
func (e *Executor) sendNotification(call assistant.ToolCall) (string, error) {
var args SendNotificationArgs
if err := call.ParseArguments(&args); err != nil {
return "", fmt.Errorf("invalid arguments: %w", err)
}
// Implement your logic here
count, err := notifications.Send(args.Title, args.Message, args.UserID)
if err != nil {
return "", fmt.Errorf("failed to send notification: %w", err)
}
// Record mutation for undo support (optional)
e.recordMutation(assist.ActionCreate, "notification", notificationID, nil, notification)
// Return JSON result
return jsonResult(map[string]any{
"sent_to": count,
"message": fmt.Sprintf("Sent notification to %d users", count),
})
}
Helper functions:
call.ParseArguments(&args)- Parse JSON arguments into structe.recordMutation(action, entityType, entityID, before, after)- Record for undojsonResult(data)- Format success response as JSON
2. Mutation Tracking
Every AI action is recorded for undo/redo:
type Mutation struct {
MessageID string // Which message triggered this
Action string // create, update, delete
EntityType string // page, collection, document
EntityID string
BeforeState string // JSON snapshot before
AfterState string // JSON snapshot after
Undone bool
}
3. Dynamic Collections
Collections have typed schemas:
schema := []schema.Field{
{Name: "title", Type: schema.Text, Required: true},
{Name: "publishedAt", Type: schema.Date},
{Name: "featured", Type: schema.Bool},
{Name: "tags", Type: schema.JSON},
}
Field types: Text, Number, Bool, Date, Email, URL, Select, Relation, File, JSON
4. Serverless Endpoints (Lua)
Endpoints execute Lua code in a sandboxed environment:
-- Access request context
local method = request.method
local path = request.path
local name = request.query.name
local body = request.body
-- Return response
return {
status = 200,
headers = { ["Content-Type"] = "application/json" },
body = json_encode({ message = "Hello, " .. name })
}
Available globals:
| Global | Purpose |
|---|---|
request.method |
HTTP method (GET, POST, etc.) |
request.path |
Request path |
request.query |
Query parameters table |
request.headers |
Request headers table |
request.body |
Request body string |
json_encode(value) |
Encode Lua value to JSON |
json_decode(string) |
Decode JSON to Lua table |
Constraints:
- 5 second execution timeout
- Sandboxed: only base, string, table, math libraries
- No file system or network access
5. Schema Validation
Documents are validated against their collection's schema:
// Schema definition
schema := []schema.Field{
{Name: "title", Type: schema.Text, Required: true, Options: map[string]any{"maxLength": 100}},
{Name: "price", Type: schema.Number, Options: map[string]any{"min": 0}},
{Name: "email", Type: schema.Email},
{Name: "status", Type: schema.Select, Options: map[string]any{"values": []string{"draft", "published"}}},
}
// Validation
err := schema.ValidateDocument(collection, data)
if verrs, ok := err.(schema.ValidationErrors); ok {
for _, e := range verrs {
fmt.Printf("%s: %s\n", e.Field, e.Message)
}
}
6. Template Functions for Pages
Pages can embed dynamic content:
<!-- In a page's HTML -->
<h1>{{site_name}}</h1>
{{range $post := documents "blog"}}
<article>
<h2>{{$post.GetString "title"}}</h2>
<p>{{$post.GetString "excerpt"}}</p>
</article>
{{else}}
<p>No posts yet.</p>
{{end}}
Available functions:
| Function | Purpose |
|---|---|
documents "collection" |
Get all documents in collection |
document "collection" "id" |
Get single document |
collection "name" |
Get collection metadata |
page "id" |
Get page by ID |
pages |
Get all pages |
published_pages |
Get published pages only |
site_name |
Site name from settings |
site_description |
Site description from settings |
Data Models
User
type User struct {
database.Model
Email string // Unique
PasswordHash string
Name string
AvatarURL string
Role string // admin, user, viewer
Verified bool
}
Page
type Page struct {
database.Model
ParentID string // For hierarchy
Title string
Description string
HTML string // Full page content
Position int // Sort order
Published bool
}
// Methods
page.Path() // Returns full URL path
page.Parent() // Returns parent page
page.Children() // Returns child pages
page.Siblings() // Returns pages at same level
Collection + Document
type Collection struct {
database.Model
Name string
Description string
Schema string // JSON field definitions
System bool // Protected from deletion
}
type Document struct {
database.Model
CollectionID string
Data string // JSON data
}
// Document methods
doc.GetString("field")
doc.GetInt("field")
doc.GetBool("field")
doc.GetTime("field")
doc.Set("field", value)
Conversation + Message
type Conversation struct {
database.Model
UserID string
Title string
Context string // JSON: current page/collection context
}
type Message struct {
database.Model
ConversationID string
Role string // user, assistant, system
Content string
ToolCalls string // JSON: AI tool calls
Status string // pending, streaming, complete
}
Controllers
Auth (/signin, /signout)
JWT-based authentication with bcrypt password hashing.
// Guard functions for routes
auth.RequireAuth // Redirects to /signin
auth.RequireAdmin // Returns 403 if not admin
Setup (/setup/*)
First-run wizard:
- Create admin account
- Configure AI provider (provider, API key, model)
- Set site name and description
Pages (/admin/pages/*)
Full CRUD with hierarchy support:
- Create/edit pages with HTML content
- Drag-drop reordering
- Publish/unpublish toggle
- Parent-child relationships
Collections (/admin/collections/*)
Dynamic content types:
- Define schema with typed fields
- CRUD for documents within collections
- Field validation (WIP)
Chat (/admin/chat/*)
AI conversation interface:
- Create new conversations
- Stream AI responses via SSE
- Execute tool calls
- Track mutations for undo
Site (/*)
Public page rendering:
- Catch-all route for published pages
- Template function execution
- 404 handling
Roadmap
Based on customer experience review, the following phases are planned:
Phase 1: Version Publishing System (Tasks 82-84)
Replace the boolean Published field with a version-based system:
- Add
Statusfield toPageContent(draft/published) - Pages derive published state from whether any version is published
- UI shows version status with publish/unpublish per version
Phase 2: WYSIWYG Editor (Tasks 85-88)
Add visual editing for non-technical users:
- Add "Editor" tab between Preview and Code tabs
- React island using Tiptap (modern ProseMirror wrapper)
- Toolbar with formatting (bold, italic, headings, lists, links)
- HTML import/export synced with Code tab
Phase 3: Blog Example (Tasks 89-96)
Create a complete working example out of the box:
- Blog listing page using template functions
- Blog view page with dynamic routing via query params
- Blog edit page with HTMX forms
- New post creation form
- All using existing collections API (not Lua - see note below)
Phase 4: OAuth for AI Providers (Future)
Research Complete (Tasks 97-98): Neither OpenAI nor Anthropic offer OAuth for third-party apps to access their APIs on behalf of users.
- OpenAI: Their Apps SDK uses OAuth 2.1 but ChatGPT is the client calling YOUR services via MCP - not the reverse
- Anthropic: Actively blocked third-party apps from using Claude OAuth tokens in January 2026
Conclusion: OAuth for AI providers is not viable. Continue with API key approach, provide tutorial content to help users get started.
Known Limitations
Lua Endpoints Have No Database Access
The Lua runtime (internal/runtime/lua.go) only provides request/response handling:
requestobject (method, path, query, headers, body)json_encode/json_decodehelpers- 5-second execution timeout
There are no database helpers. Lua endpoints cannot create, read, update, or delete documents. For CRUD operations, use:
- The collections controller API (
/admin/collections/{id}/documents) - HTMX forms in pages that POST to the collections API
A future task (93) may add optional database helpers to Lua.
Template Functions Don't Have Request Access
The render functions in internal/render/ don't receive *http.Request. To access query parameters in page templates, the request must be passed through the template context (Task 94).
Implementation Status
Complete
- User authentication (JWT + bcrypt)
- Setup wizard
- Page CRUD with hierarchy
- Page versioning (content history, restore, diff view)
- Collection CRUD with schemas
- Document CRUD with schema validation
- Endpoint CRUD with Lua execution
- AI chat with SSE streaming
- Tool execution framework (21 tools)
- Mutation tracking
- ACL system (rules, middleware, enforcement)
- Admin UI (DaisyUI + HTMX)
- Public page rendering with ACL checks
- File upload with validation
- Rate limiting
- Audit logging
- Health endpoint
Partial
- AI integration (framework ready, needs provider testing)
- Undo/redo (mutations tracked, UI not wired)
Not Started (Roadmap Items)
- Version publishing system (Phase 1)
- WYSIWYG editor (Phase 2)
- Blog example seed data (Phase 3)
- Full-text search
- Backup/restore
- Python/Node endpoint runtimes
Environment Variables
| Variable | Required | Description |
|---|---|---|
PORT |
No | Server port (default: 5000) |
ENV |
No | Set to "production" for prod mode |
AUTH_SECRET |
Yes | JWT signing secret |
Running Locally
cd website
go run .
# Open http://localhost:5000
# Complete setup wizard on first run
Deployment
# From project root
go run ./cmd/launch --new website
The Dockerfile uses multi-stage build:
- Builder:
golang:1.24-bookworm(CGO for libsql) - Runtime:
debian:bookworm-slim - Data directory:
/data/(mount as volume)