Building a SaaS API Backend with Aerostack
This guide shows how to build a complete SaaS API backend with user management, data storage, caching, and background jobs—all in under an hour.
Use Case: Project Management SaaS
Requirements:
- User authentication (handled by Aerostack)
- Projects and tasks CRUD APIs
- Real-time collaboration updates
- File attachments for tasks
- Email notifications for task assignments
- Search with caching
Traditional Approach: 3-5 days of setup
Aerostack Approach: 30-60 minutes
Step 1: Database Schema
First, create your tables in the Aerostack admin:
CREATE TABLE projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'todo',
assigned_to INTEGER,
due_date INTEGER,
attachment_url TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id)
);
CREATE INDEX idx_tasks_project ON tasks(project_id);
CREATE INDEX idx_tasks_status ON tasks(status);Step 2: Create Project API
Endpoint: POST /custom/create-project
export default async function(sdk, event) {
const { name, description } = event.data;
const user_id = event.user?.id; // Auto-populated by auth
// Validate
if (!name || name.length < 3) {
throw new Error('Project name must be at least 3 characters');
}
const now = Date.now();
// Create project
const result = await sdk.db.query(`
INSERT INTO projects (user_id, name, description, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
`, [user_id, name, description, now, now]);
const project = {
id: result.lastInsertRowid,
user_id,
name,
description,
created_at: now,
updated_at: now
};
// Invalidate user's project list cache
await sdk.cache.delete(`projects:user:${user_id}`);
return { project };
}Step 3: List Projects with Caching
Endpoint: GET /custom/list-projects
export default async function(sdk, event) {
const user_id = event.user?.id;
const cacheKey = `projects:user:${user_id}`;
// Check cache
const cached = await sdk.cache.get(cacheKey);
if (cached) {
return { projects: cached, from_cache: true };
}
// Query database
const projects = await sdk.db.query(`
SELECT id, name, description, created_at, updated_at
FROM projects
WHERE user_id = ?
ORDER BY updated_at DESC
`, [user_id]);
// Cache for 5 minutes
await sdk.cache.set(cacheKey, projects, 300);
return { projects, from_cache: false };
}Step 4: Create Task with File Upload
Endpoint: POST /custom/create-task
export default async function(sdk, event) {
const { project_id, title, description, assigned_to, due_date, attachment } = event.data;
const user_id = event.user?.id;
// Validate ownership
const [project] = await sdk.db.query(
'SELECT id FROM projects WHERE id = ? AND user_id = ?',
[project_id, user_id]
);
if (!project) {
throw new Error('Project not found or access denied');
}
let attachment_url = null;
// Handle file upload if present
if (attachment?.file_base64 && attachment?.filename) {
const key = `tasks/${project_id}/${Date.now()}_${attachment.filename}`;
attachment_url = await sdk.storage.upload(key, attachment.file_base64, {
contentType: attachment.content_type || 'application/octet-stream'
});
}
const now = Date.now();
// Create task
const result = await sdk.db.query(`
INSERT INTO tasks (
project_id, title, description, assigned_to,
due_date, attachment_url, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [project_id, title, description, assigned_to, due_date, attachment_url, now, now]);
const task = {
id: result.lastInsertRowid,
project_id,
title,
description,
status: 'todo',
assigned_to,
due_date,
attachment_url,
created_at: now,
updated_at: now
};
// Send email notification asynchronously
if (assigned_to) {
await sdk.queue.enqueue('send-task-assignment-email', {
task_id: task.id,
assigned_to,
title
});
}
// Invalidate cache
await sdk.cache.delete(`tasks:project:${project_id}`);
return { task };
}Step 5: Background Email Worker
Worker: send-task-assignment-email
export async function sendTaskAssignmentEmail(sdk, job) {
const { task_id, assigned_to, title } = job.data;
// Get assignee details
const [user] = await sdk.db.query(
'SELECT email, name FROM users WHERE id = ?',
[assigned_to]
);
if (!user) return { error: 'User not found' };
// Get email API key from secrets
const sendgridKey = await sdk.secrets.get('SENDGRID_API_KEY');
// Send email
await sdk.fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sendgridKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
personalizations: [{
to: [{ email: user.email, name: user.name }]
}],
from: { email: '[email protected]', name: 'Your App' },
subject: `New task assigned: ${title}`,
content: [{
type: 'text/html',
value: `
<h2>You've been assigned a new task</h2>
<p><strong>${title}</strong></p>
<p><a href="https://yourapp.com/tasks/${task_id}">View Task</a></p>
`
}]
})
});
return { success: true, sent_to: user.email };
}Step 6: Search Tasks with AI
Endpoint: POST /custom/search-tasks
export default async function(sdk, event) {
const { query, project_id } = event.data;
const user_id = event.user?.id;
// Verify access
const [project] = await sdk.db.query(
'SELECT id FROM projects WHERE id = ? AND user_id = ?',
[project_id, user_id]
);
if (!project) {
throw new Error('Project not found');
}
const cacheKey = `search:${project_id}:${query}`;
// Check cache
const cached = await sdk.cache.get(cacheKey);
if (cached) return { results: cached };
// Basic SQL search
const sqlResults = await sdk.db.query(`
SELECT * FROM tasks
WHERE project_id = ?
AND (title LIKE ? OR description LIKE ?)
ORDER BY created_at DESC
LIMIT 20
`, [project_id, `%${query}%`, `%${query}%`]);
// Use AI to rank by relevance if query is complex
if (query.split(' ').length > 2) {
const aiRanking = await sdk.ai.chat('@cf/meta/llama-3-8b-instruct', [
{
role: 'system',
content: 'Rank tasks by relevance to the search query. Return task IDs in order of relevance.'
},
{
role: 'user',
content: `Query: "${query}"\n\nTasks: ${JSON.stringify(sqlResults)}`
}
]);
// Reorder results based on AI ranking
// (simplified - in production you'd parse AI response properly)
}
// Cache for 10 minutes
await sdk.cache.set(cacheKey, sqlResults, 600);
return { results: sqlResults };
}Complete API Endpoints
In 30-60 minutes, you’ve built:
✅ POST /custom/create-project - Create projects
✅ GET /custom/list-projects - List with caching
✅ POST /custom/create-task - Create with file uploads
✅ POST /custom/search-tasks - AI-powered search
✅ Background worker - Email notifications
No infrastructure setup. No DevOps. Just code.
What’s Included Automatically
- ✅ Database - Scoped to your project
- ✅ Cache - 5-10 minute TTLs for list endpoints
- ✅ Queue - Background email workers
- ✅ Storage - File attachments with CDN
- ✅ AI - Smart search ranking
- ✅ Auth - User authentication via
event.user - ✅ Rate Limiting - Per-endpoint configuration
- ✅ Monitoring - Built-in logs and metrics
Next Steps
- Add real-time updates with webhooks
- Implement task comments and activity feeds
- Add analytics and reporting APIs
- Build mobile/web frontend using these APIs
Ready to build your SaaS backend?