496 lines
11 KiB
Markdown
496 lines
11 KiB
Markdown
# Backend Integration Guide
|
|
|
|
This guide explains how to use the backend system in Braindrops.
|
|
|
|
## Overview
|
|
|
|
Braindrops now includes an optional self-hosted backend powered by Bun. Each drop can optionally have:
|
|
|
|
- **REST API endpoints** - For data persistence and external API calls
|
|
- **WebSocket handlers** - For real-time communication
|
|
- **SQLite database** - Shared across all drops with namespaced tables
|
|
|
|
## Quick Start
|
|
|
|
### 1. Develop a Drop with Backend
|
|
|
|
```bash
|
|
npm run dev:drops image-melody
|
|
```
|
|
|
|
This automatically starts:
|
|
- Drop frontend on `http://localhost:3000`
|
|
- CMS on `http://localhost:8787`
|
|
- Backend API on `http://localhost:3001/api/image-melody`
|
|
- WebSocket on `ws://localhost:3001/ws/image-melody`
|
|
|
|
### 2. Add Backend to a Drop
|
|
|
|
Create a `backend/` folder in your drop:
|
|
|
|
```bash
|
|
drops/my-drop/
|
|
├── src/ # Frontend
|
|
├── backend/ # Backend (NEW)
|
|
│ ├── api.js # REST endpoints
|
|
│ ├── schema.sql # Database schema
|
|
│ └── websocket.js # WebSocket handler (optional)
|
|
└── public/
|
|
```
|
|
|
|
### 3. Use the Template
|
|
|
|
New drops created with `npm run create` will ask if you want to include a backend. The template provides:
|
|
- Example CRUD API
|
|
- Sample database schema
|
|
- WebSocket handler
|
|
- README with documentation
|
|
|
|
## Creating a Backend
|
|
|
|
### Step 1: Define Your Database Schema
|
|
|
|
Create `backend/schema.sql`:
|
|
|
|
```sql
|
|
-- Use naming convention: <drop-name>_<table-name>
|
|
CREATE TABLE IF NOT EXISTS my_drop_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
data TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_my_drop_items_created
|
|
ON my_drop_items(created_at);
|
|
```
|
|
|
|
### Step 2: Create Your API
|
|
|
|
Create `backend/api.js`:
|
|
|
|
```javascript
|
|
import { dbHelpers } from '../../../backend/src/db.js'
|
|
const { query, queryOne, run } = dbHelpers
|
|
|
|
export const routes = {
|
|
// GET /api/my-drop/items
|
|
'GET /items': async (req) => {
|
|
const items = query('SELECT * FROM my_drop_items ORDER BY created_at DESC')
|
|
return Response.json({ items, count: items.length })
|
|
},
|
|
|
|
// GET /api/my-drop/items/:id
|
|
'GET /items/:id': async (req, params) => {
|
|
const item = queryOne('SELECT * FROM my_drop_items WHERE id = ?', [params.id])
|
|
|
|
if (!item) {
|
|
return Response.json({ error: 'Not found' }, { status: 404 })
|
|
}
|
|
|
|
return Response.json(item)
|
|
},
|
|
|
|
// POST /api/my-drop/items
|
|
'POST /items': async (req) => {
|
|
const body = await req.json()
|
|
|
|
if (!body.name || !body.data) {
|
|
return Response.json({ error: 'Missing fields' }, { status: 400 })
|
|
}
|
|
|
|
const result = run(
|
|
'INSERT INTO my_drop_items (name, data) VALUES (?, ?)',
|
|
[body.name, JSON.stringify(body.data)]
|
|
)
|
|
|
|
return Response.json({ id: result.lastInsertRowid, success: true }, { status: 201 })
|
|
},
|
|
|
|
// DELETE /api/my-drop/items/:id
|
|
'DELETE /items/:id': async (req, params) => {
|
|
const result = run('DELETE FROM my_drop_items WHERE id = ?', [params.id])
|
|
|
|
if (result.changes === 0) {
|
|
return Response.json({ error: 'Not found' }, { status: 404 })
|
|
}
|
|
|
|
return Response.json({ success: true })
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 3: Add WebSocket (Optional)
|
|
|
|
Create `backend/websocket.js`:
|
|
|
|
```javascript
|
|
export const websocket = {
|
|
open(ws) {
|
|
console.log('Client connected')
|
|
ws.send(JSON.stringify({ type: 'connected', message: 'Welcome!' }))
|
|
},
|
|
|
|
message(ws, message) {
|
|
const data = JSON.parse(message)
|
|
|
|
// Broadcast to all connected clients
|
|
ws.publish('my-drop', JSON.stringify({
|
|
type: 'broadcast',
|
|
data: data
|
|
}))
|
|
},
|
|
|
|
close(ws) {
|
|
console.log('Client disconnected')
|
|
}
|
|
}
|
|
```
|
|
|
|
## Using the Backend in Your Drop
|
|
|
|
### REST API
|
|
|
|
From your drop's frontend (`src/App.vue` or any component):
|
|
|
|
```javascript
|
|
// GET request
|
|
async function loadItems() {
|
|
const response = await fetch('/api/my-drop/items')
|
|
const data = await response.json()
|
|
console.log(data.items)
|
|
}
|
|
|
|
// POST request
|
|
async function createItem(name, data) {
|
|
const response = await fetch('/api/my-drop/items', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, data })
|
|
})
|
|
const result = await response.json()
|
|
console.log('Created ID:', result.id)
|
|
}
|
|
|
|
// DELETE request
|
|
async function deleteItem(id) {
|
|
const response = await fetch(`/api/my-drop/items/${id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
const result = await response.json()
|
|
console.log('Deleted:', result.success)
|
|
}
|
|
```
|
|
|
|
### WebSocket
|
|
|
|
```javascript
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
export default {
|
|
setup() {
|
|
const ws = ref(null)
|
|
const messages = ref([])
|
|
|
|
onMounted(() => {
|
|
// Connect to WebSocket
|
|
ws.value = new WebSocket('ws://localhost:3001/ws/my-drop')
|
|
|
|
ws.value.onopen = () => {
|
|
console.log('Connected to WebSocket')
|
|
}
|
|
|
|
ws.value.onmessage = (event) => {
|
|
const data = JSON.parse(event.data)
|
|
messages.value.push(data)
|
|
}
|
|
|
|
ws.value.onerror = (error) => {
|
|
console.error('WebSocket error:', error)
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (ws.value) {
|
|
ws.value.close()
|
|
}
|
|
})
|
|
|
|
function sendMessage(data) {
|
|
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
|
ws.value.send(JSON.stringify(data))
|
|
}
|
|
}
|
|
|
|
return { messages, sendMessage }
|
|
}
|
|
}
|
|
```
|
|
|
|
## Database Helpers
|
|
|
|
The backend provides helper functions for database operations:
|
|
|
|
```javascript
|
|
import { dbHelpers } from '../../../backend/src/db.js'
|
|
const { query, queryOne, run, getDb } = dbHelpers
|
|
|
|
// query(sql, params) - Get all results
|
|
const items = query('SELECT * FROM my_drop_items WHERE name LIKE ?', ['%test%'])
|
|
|
|
// queryOne(sql, params) - Get first result
|
|
const item = queryOne('SELECT * FROM my_drop_items WHERE id = ?', [1])
|
|
|
|
// run(sql, params) - Execute INSERT/UPDATE/DELETE
|
|
const result = run('INSERT INTO my_drop_items (name, data) VALUES (?, ?)', ['test', 'data'])
|
|
console.log(result.lastInsertRowid) // Get auto-increment ID
|
|
console.log(result.changes) // Number of affected rows
|
|
|
|
// getDb() - Get raw database instance
|
|
const db = getDb()
|
|
const stmt = db.prepare('SELECT * FROM my_drop_items')
|
|
const items = stmt.all()
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
### Setting Up
|
|
|
|
1. Copy the example file:
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
2. Add your variables:
|
|
```bash
|
|
PRODUCTION_URL=https://yourdomain.com
|
|
OPENAI_API_KEY=sk-...
|
|
WEATHER_API_KEY=...
|
|
```
|
|
|
|
3. Access in your backend code:
|
|
```javascript
|
|
const apiKey = process.env.OPENAI_API_KEY
|
|
```
|
|
|
|
### Important Notes
|
|
|
|
- `.env` is gitignored and won't be committed
|
|
- Don't put API keys in frontend code (use backend as proxy)
|
|
- Environment variables are only available in backend code, not frontend
|
|
|
|
## Example: External API Proxy
|
|
|
|
To call external APIs without exposing keys:
|
|
|
|
```javascript
|
|
// backend/api.js
|
|
export const routes = {
|
|
'POST /weather': async (req) => {
|
|
const body = await req.json()
|
|
const { city } = body
|
|
|
|
// Call external API with secret key
|
|
const response = await fetch(
|
|
`https://api.weather.com/data?city=${city}&key=${process.env.WEATHER_API_KEY}`
|
|
)
|
|
const data = await response.json()
|
|
|
|
return Response.json(data)
|
|
}
|
|
}
|
|
```
|
|
|
|
Then from your frontend:
|
|
```javascript
|
|
const response = await fetch('/api/my-drop/weather', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ city: 'London' })
|
|
})
|
|
const weather = await response.json()
|
|
```
|
|
|
|
## Production Deployment
|
|
|
|
### 1. Build Everything
|
|
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
This will:
|
|
- Build all drops
|
|
- Build the main app
|
|
- Build the backend (if you add the script)
|
|
|
|
### 2. Deploy Static Files
|
|
|
|
Upload `app/dist/` to your static host (nginx, Apache, etc.)
|
|
|
|
### 3. Deploy Backend
|
|
|
|
```bash
|
|
# Build backend
|
|
node bin/buildBackend.js
|
|
|
|
# Copy to server
|
|
scp -r backend/dist user@server:/var/www/braindrops/backend
|
|
|
|
# On server
|
|
cd /var/www/braindrops/backend
|
|
bun install --production
|
|
./start.sh
|
|
```
|
|
|
|
### 4. Configure nginx
|
|
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name yourdomain.com;
|
|
|
|
# Static files
|
|
location / {
|
|
root /var/www/braindrops/static;
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
# Backend API
|
|
location /api/ {
|
|
proxy_pass http://localhost:3001;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
}
|
|
|
|
# WebSocket
|
|
location /ws/ {
|
|
proxy_pass http://localhost:3001;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Set Production Environment Variables
|
|
|
|
On your server:
|
|
```bash
|
|
cd /var/www/braindrops/backend
|
|
nano .env
|
|
```
|
|
|
|
Add:
|
|
```bash
|
|
NODE_ENV=production
|
|
PRODUCTION_URL=https://yourdomain.com
|
|
# Your API keys...
|
|
```
|
|
|
|
### 6. Run Backend with Process Manager
|
|
|
|
Using PM2:
|
|
```bash
|
|
npm install -g pm2
|
|
pm2 start start.sh --name braindrops-backend
|
|
pm2 save
|
|
pm2 startup
|
|
```
|
|
|
|
Or use systemd, supervisor, etc.
|
|
|
|
## Best Practices
|
|
|
|
### Security
|
|
|
|
1. **Never expose API keys in frontend code**
|
|
2. **Validate all input** on the backend
|
|
3. **Use HTTPS** in production
|
|
4. **Sanitize database inputs** (use parameterized queries)
|
|
5. **Add rate limiting** if needed (use nginx or middleware)
|
|
|
|
### Database
|
|
|
|
1. **Use the naming convention** `<drop-name>_<table-name>`
|
|
2. **Add indexes** for frequently queried columns
|
|
3. **Store JSON as TEXT** using `JSON.stringify()`
|
|
4. **Use transactions** for multi-step operations
|
|
5. **Backup regularly** (copy `braindrops.db` file)
|
|
|
|
### API Design
|
|
|
|
1. **Use proper HTTP methods** (GET, POST, PUT, DELETE)
|
|
2. **Return appropriate status codes** (200, 201, 400, 404, 500)
|
|
3. **Use consistent response format** `{ success: true, data: ... }`
|
|
4. **Handle errors gracefully** with try/catch
|
|
5. **Document your endpoints** in comments
|
|
|
|
### WebSocket
|
|
|
|
1. **Validate messages** before broadcasting
|
|
2. **Use JSON** for structured data
|
|
3. **Handle disconnections** gracefully
|
|
4. **Implement reconnect logic** in frontend
|
|
5. **Namespace channels** by drop name
|
|
|
|
## Troubleshooting
|
|
|
|
### Backend not starting
|
|
|
|
Check if Bun is installed:
|
|
```bash
|
|
bun --version
|
|
```
|
|
|
|
Install if needed:
|
|
```bash
|
|
curl -fsSL https://bun.sh/install | bash
|
|
```
|
|
|
|
### Port already in use
|
|
|
|
Change the port in `.env`:
|
|
```bash
|
|
PORT=3002
|
|
```
|
|
|
|
### Database locked
|
|
|
|
SQLite is running in WAL mode which should prevent most locking issues. If you still encounter locks:
|
|
1. Ensure only one backend instance is running
|
|
2. Close any database browser tools
|
|
3. Delete `.db-wal` and `.db-shm` files
|
|
|
|
### CORS errors
|
|
|
|
Make sure your frontend URL is allowed in `backend/src/shared/middleware.js`.
|
|
|
|
For development, these are allowed by default:
|
|
- `http://localhost:3000` (drop dev)
|
|
- `http://localhost:5173` (app dev)
|
|
|
|
For production, set `PRODUCTION_URL` in `.env`.
|
|
|
|
## Examples
|
|
|
|
See `drops/image-melody/backend/` for a complete working example with:
|
|
- Database schema for storing melodies
|
|
- CRUD API for managing melodies
|
|
- WebSocket for broadcasting melody plays
|
|
- Full integration with frontend
|
|
|
|
## Getting Help
|
|
|
|
- Check `backend/README.md` for backend-specific docs
|
|
- Check `template/backend/README.md` for template usage
|
|
- Review the image-melody example
|
|
- Check Bun documentation: https://bun.sh/docs
|
|
|
|
## Next Steps
|
|
|
|
1. Try the example: `npm run dev:drops image-melody`
|
|
2. Test the API with curl or your browser
|
|
3. Create your own backend for a drop
|
|
4. Experiment with WebSocket real-time features
|
|
5. Deploy to production!
|