Headless SaaS for AI Agents
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β The future of SaaS is not browser-based applications for humans. β
β It's headless services for AI agents. β
β β
β SaaSkit generates complete SaaS products from a single <App/> component: β
β β
β β’ Terminal UI (React Ink) Primary interface for agents & humans β
β β’ REST API Programmatic access β
β β’ TypeScript SDK Type-safe client libraries β
β β’ MCP Server Native AI agent integration β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AI agents are fundamentally changing how software is consumed. The traditional SaaS modelβbrowser-based UIs designed for human point-and-click interactionβis becoming obsolete for an increasing number of use cases.
What agents need:
- Clean, parseable output (not pixel-perfect screenshots)
- Structured data alongside human-readable presentation
- CLI interfaces that can be invoked programmatically
- MCP servers for native tool integration
- Self-documenting APIs with predictable patterns
What SaaSkit provides:
- A single declarative
<App/>component that generates everything - Terminal-first UI that renders beautifully AND degrades to markdown
- Automatic API, SDK, and MCP generation from your schema
- Output that can be copy-pasted, piped, and parsed
npx create-saas-app my-app
cd my-app
npm startDefine your entire SaaS product in a single file:
import { App } from 'saaskit'
export default (
<App name="todos">
<Task title done priority="low | medium | high" />
</App>
)That's it. The component name is the resource. Attributes are the fields. Types are inferred or declared inline.
<Task
title // text (required - first field)
description? // text (optional)
done // boolean (inferred from name)
priority="low | medium | high" // select (pipe-separated options)
dueDate:date // explicit type
assignee->User // relation to another resource
tags->Tag[] // many relation
/>When you need more control:
import { App, Text, Boolean, Select, Relation } from 'saaskit'
export default (
<App name="todos">
<Task>
<Text name="title" required />
<Text name="description" />
<Boolean name="done" default={false} />
<Select name="priority" options={['low', 'medium', 'high']} default="medium" />
<Relation name="assignee" to="User" />
</Task>
</App>
)This single definition generates:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β todos v1.0.0 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β TASKS 3 items β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Title β Done β Priority β Actions β β
β βββββββββββββββββββββββββββββββββββΌβββββββΌβββββββββββΌβββββββββββββββββββ€ β
β β Build the MVP β β β high β [edit] [delete] β β
β β Write documentation β β β medium β [edit] [delete] β β
β β Deploy to production β β β high β [edit] [delete] β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β [n] New task [/] Search [?] Help [q] Quit β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The same view, when colors/boxes aren't available:
# todos v1.0.0
## TASKS (3 items)
| Title | Done | Priority |
|----------------------|------|----------|
| Build the MVP | [ ] | high |
| Write documentation | [ ] | medium |
| Deploy to production | [x] | high |
Commands: [n]ew [/]search [?]help [q]uit$ todos list
$ todos create --title "New task" --priority high
$ todos update abc123 --done true
$ todos delete abc123
$ todos search "MVP"GET /api/tasks
POST /api/tasks
GET /api/tasks/:id
PUT /api/tasks/:id
DELETE /api/tasks/:idimport { TodosClient } from 'todos-sdk'
const client = new TodosClient({ apiKey: '...' })
const tasks = await client.tasks.list()
const task = await client.tasks.create({ title: 'New task', priority: 'high' })
await client.tasks.update(task.id, { done: true })$ todos mcp
# Starts MCP server on stdioAuto-generated tools:
{
"tools": [
{
"name": "todos_tasks_list",
"description": "List all tasks",
"inputSchema": { "type": "object", "properties": {} }
},
{
"name": "todos_tasks_create",
"description": "Create a new task",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"done": { "type": "boolean" },
"priority": { "enum": ["low", "medium", "high"] }
},
"required": ["title"]
}
}
]
}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β <App/> β
β β β
β βββββββββββββ΄ββββββββββββ β
β β β β
β <Resource/> <Resource/> β
β β β β
β βββββββββββ΄ββββββββββ ββββββββββ΄βββββββββ β
β β β β β β β β
β <Field> <Field> <Field> ... β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Generators β
ββββββββββββββββ¬βββββββββββββββ¬ββββββββββββββ¬βββββββββββ€
β Terminal UI β REST API β SDK β MCP β
β (React Ink) β (Hono) β (TypeScript)β Server β
ββββββββββββββββ΄βββββββββββββββ΄ββββββββββββββ΄βββββββββββ
Each component becomes a resource with automatic:
- CRUD operations (create, read, update, delete, list)
- Search functionality
- CLI commands
- API endpoints
- SDK methods
- MCP tools
<User name email:email bio? createdAt:auto />| Syntax | Meaning | Example |
|---|---|---|
name |
Required text | <Task title /> |
name? |
Optional text | <Task description? /> |
name:type |
Explicit type | <Task dueDate:date /> |
name:unique |
Unique constraint | <User email:unique /> |
name:auto |
Auto-generated | <Task createdAt:auto /> |
name="a | b | c" |
Select options | <Task status="todo | done" /> |
name->Resource |
Relation | <Task assignee->User /> |
name->Resource[] |
Many relation | <Task tags->Tag[] /> |
name->Resource? |
Optional relation | <Task assignee->User? /> |
Common field names are auto-typed:
| Name Pattern | Inferred Type |
|---|---|
done, completed, active, enabled |
Boolean |
email |
|
url, website, link |
URL |
createdAt, updatedAt, *At |
DateTime |
count, amount, price, value |
Number |
*Id |
Relation (if resource exists) |
Views define terminal presentation:
<Task title done priority="low | medium | high">
<View name="list" default columns={['title', 'done', 'priority']} />
<View name="board" groupBy="priority" />
<View name="detail" />
</Task>Custom operations beyond CRUD:
<Task title done>
<Action name="complete-all" run={async (ctx) => {
await ctx.db.task.updateMany({ done: true })
return 'All tasks completed'
}} />
</Task>$ todos task complete-all
β All tasks completedReact to data changes:
<Task title done priority="low | medium | high">
<Hook on="create" run={async (task, ctx) => {
if (task.priority === 'high') {
await ctx.notify(`High priority: ${task.title}`)
}
}} />
</Task>Built on React Ink, designed for both human readability and agent parseability:
<Table
data={tasks}
columns={[
{ key: 'title', label: 'Title', width: 30 },
{ key: 'done', label: 'Done', render: (v) => v ? 'β' : 'β' },
{ key: 'priority', label: 'Priority' }
]}
/><Form
onSubmit={handleCreate}
fields={[
{ name: 'title', type: 'text', required: true },
{ name: 'priority', type: 'select', options: ['low', 'medium', 'high'] }
]}
/><Detail
data={task}
fields={['title', 'description', 'done', 'priority', 'createdAt']}
/>Output:
βββββββββββββββββββββββββββββββββββββββββββ
β Task: Build the MVP β
βββββββββββββββββββββββββββββββββββββββββββ€
β Title: Build the MVP β
β Description: Create initial version β
β Done: β β
β Priority: high β
β Created: 2024-01-15 10:30:00 β
βββββββββββββββββββββββββββββββββββββββββββ
<Box title="Stats" border="single">
<Text>Total: 42</Text>
<Text>Completed: 28</Text>
<Text>Pending: 14</Text>
</Box><Spinner text="Loading..." />
<Progress value={0.75} label="Uploading" />Every command supports multiple output formats:
$ todos list # Default: Terminal UI
$ todos list --format table # ASCII table
$ todos list --format json # JSON (for piping)
$ todos list --format markdown # Markdown
$ todos list --format csv # CSV
$ todos list --format yaml # YAMLThis is critical for agent interoperability. An agent can request JSON for processing:
$ todos list --format json | jq '.[] | select(.priority == "high")'Every SaaSkit app is an MCP server. Start it with:
$ todos mcpConfigure in Claude Desktop:
{
"mcpServers": {
"todos": {
"command": "todos",
"args": ["mcp"]
}
}
}Now Claude can natively manage your todos:
"Create a high-priority task to review the PR" "Show me all incomplete tasks" "Mark the deployment task as done"
<App
name="todos"
description="Task management"
version="1.0.0"
// Database
database="sqlite://./data.db" // or postgres://, mysql://, etc.
// Authentication
auth={{
providers: ['api-key', 'oauth'],
apiKeyHeader: 'X-API-Key'
}}
// Rate limiting
rateLimit={{
requests: 100,
window: '1m'
}}
// Theming (terminal colors)
theme={{
primary: 'cyan',
success: 'green',
warning: 'yellow',
error: 'red'
}}
>-
Agents excel at CLI: Frontier models are remarkably good at understanding and generating CLI commands. They struggle with screenshots and pixel coordinates.
-
Composability: Unix philosophy. Pipe outputs, chain commands, integrate with existing tools.
-
Accessibility: Terminal output works over SSH, in containers, in CI/CD, in scripts. Browser UIs don't.
-
Bandwidth efficiency: Text is tiny. No asset loading, no JavaScript bundles, no layout thrashing.
-
Determinism: Same command, same output. No race conditions from async UI rendering.
-
Markdown parity: Terminal tables and boxes map 1:1 to markdown. Easy to document, share, and discuss.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Today: β
β Human β Browser β SaaS UI β Database β
β β
β Tomorrow: β
β Agent β CLI/MCP β Headless SaaS β Database β
β Human β Agent β CLI/MCP β Headless SaaS β Database β
β β
β The human is still in the loop, but increasingly through natural β
β language to agents rather than direct UI manipulation. β
β β
β SaaSkit is infrastructure for this future. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
<App name="crm">
<Contact name email:email company? status="lead | prospect | customer | churned" />
<Deal title contact->Contact value:number stage="discovery | proposal | negotiation | closed" />
<Activity contact->Contact type="call | email | meeting | note" notes? />
</App>$ crm contact create --name "Jane Smith" --email "jane@acme.com" --company "Acme Inc"
$ crm deal create --title "Enterprise License" --contact jane-smith --value 50000
$ crm activity create --contact jane-smith --type call --notes "Discussed pricing"
$ crm contact list --status customer<App name="issues">
<Issue title body? status="open | in-progress | resolved | closed" priority="critical | high | medium | low" assignee->User? labels->Label[] />
<User name email:email />
<Label name color? />
</App><App name="cms">
<Post title slug:unique content:markdown status="draft | published | archived" author->Author tags->Tag[] publishedAt:date? />
<Author name email:email bio? />
<Tag name:unique />
</App><App name="notes">
<Note content />
</App>That's a complete app. One resource, one field. Generates CLI, API, SDK, MCPβeverything.
| Feature | Traditional SaaS | SaaSkit |
|---|---|---|
| Primary interface | Browser UI | Terminal CLI |
| Secondary interfaces | Mobile app, API | API, SDK, MCP |
| Target user | Human | AI Agent + Human |
| Output format | Rendered pixels | Structured text |
| Composability | Low (walled garden) | High (Unix pipes) |
| Automation | Selenium/Playwright | Native CLI/API |
| Documentation | Screenshots | Markdown (identical to UI) |
- Core
<App/>component - Resource definition and CRUD
- React Ink terminal UI
- CLI generation
- REST API generation
- TypeScript SDK generation
- MCP server generation
- Authentication providers
- Webhooks
- Real-time subscriptions (WebSocket)
- Multi-tenancy
- Plugin system
- Marketplace
"The best interface is no interface." β Golden Krishna
For AI agents, this is literally true. They don't need interfacesβthey need protocols. SaaSkit provides protocols (CLI, API, MCP) that happen to also render as beautiful terminal UIs for the humans who are still in the loop.
The terminal is not a step backward. It's a return to fundamentals: text in, text out, infinitely composable, universally accessible, and perfectly suited for the age of AI agents.
MIT
SaaSkit β Headless SaaS for the Age of AI Agents