| .. | ||
| src | ||
| .gitignore | ||
| bun.lockb | ||
| package.json | ||
| README.md | ||
Braindrops Backend
Self-hosted Bun backend server for the Braindrops drops system.
Features
- REST API - Automatic endpoint loading from drop backends
- WebSocket - Real-time bidirectional communication
- SQLite Database - Lightweight, file-based storage
- Auto-Discovery - Automatically loads drop backends on startup
- Hot Reload - Automatically reloads when code changes (dev mode)
- CORS Support - Configured for local development and production
Architecture
backend/
├── src/
│ ├── index.js # Main server entry point
│ ├── db.js # SQLite database utilities
│ ├── loader.js # Drop backend auto-discovery
│ ├── router.js # API routing
│ ├── websocket.js # WebSocket handling
│ └── shared/
│ └── middleware.js # CORS and other middleware
├── db/
│ └── braindrops.db # SQLite database (gitignored)
└── package.json
Development
Starting the Backend
The backend is automatically started when you run:
npm run dev:drops <drop-name>
This starts:
- Drop dev server on port 3000
- CMS on port 8787
- Backend on port 3001 (only loads the specified drop's backend)
Standalone Backend
To run the backend independently:
npm run dev:backend
This loads ALL drop backends at once.
Drop Backend Structure
Each drop can optionally have a backend/ folder:
drops/<drop-name>/
├── src/ # Frontend code
├── backend/ # Backend code (optional)
│ ├── api.js # REST endpoints
│ ├── schema.sql # Database schema
│ └── websocket.js # WebSocket handler (optional)
└── public/
Example API (api.js)
import { dbHelpers } from '../../../backend/src/db.js'
const { query, queryOne, run } = dbHelpers
export const routes = {
'GET /items': async (req) => {
const items = query('SELECT * FROM mydrop_items')
return Response.json({ items })
},
'POST /items': async (req) => {
const body = await req.json()
const result = run('INSERT INTO mydrop_items (data) VALUES (?)', [body.data])
return Response.json({ id: result.lastInsertRowid })
}
}
Example Schema (schema.sql)
CREATE TABLE IF NOT EXISTS mydrop_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Example WebSocket (websocket.js)
export const websocket = {
open(ws) {
ws.send(JSON.stringify({ type: 'connected' }))
},
message(ws, message) {
const data = JSON.parse(message)
// Broadcast to all clients
ws.publish('mydrop', JSON.stringify(data))
}
}
API Endpoints
Health Check
GET /health
Returns server status and loaded drops.
Drop APIs
/api/<drop-name>/*
Each drop's API is namespaced under /api/<drop-name>/.
Example:
# Get melodies from image-melody drop
curl http://localhost:3001/api/image-melody/melodies
# Create a new melody
curl -X POST http://localhost:3001/api/image-melody/melodies \
-H "Content-Type: application/json" \
-d '{"name":"My Melody","imageData":{},"melodyData":{}}'
WebSocket
ws://<host>:<port>/ws/<drop-name>
Example:
const ws = new WebSocket('ws://localhost:3001/ws/image-melody')
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Received:', data)
}
ws.send(JSON.stringify({ type: 'hello' }))
Database
The backend uses SQLite for data storage:
- Database file:
backend/db/braindrops.db - Tables use naming convention:
<drop-name>_<table-name> - Schemas are auto-applied on startup
- WAL mode enabled for better concurrency
Database Helpers
Available in drop backends via import:
import { dbHelpers } from '../../../backend/src/db.js'
const { query, queryOne, run } = dbHelpers
// Get all rows
const items = query('SELECT * FROM mydrop_items')
// Get single row
const item = queryOne('SELECT * FROM mydrop_items WHERE id = ?', [1])
// Insert/Update/Delete
const result = run('INSERT INTO mydrop_items (data) VALUES (?)', ['test'])
console.log(result.lastInsertRowid) // Get inserted ID
Environment Variables
Create a .env file in the root directory:
# Production URL for CORS
PRODUCTION_URL=https://yourdomain.com
# Optional: Custom port
PORT=3001
# Your API keys
OPENAI_API_KEY=your-key
SOME_SERVICE_KEY=your-key
Access in code:
const apiKey = process.env.OPENAI_API_KEY
Production Build
Build for production:
node bin/buildBackend.js
This creates backend/dist/ with:
- All source files
- package.json
- Database directory
- Start script
Deploying to Production
- Copy
backend/dist/to your server - Install dependencies:
cd backend/dist && bun install - Set environment variables
- Start:
./start.sh
nginx Configuration
# API proxy
location /api/ {
proxy_pass http://localhost:3001;
}
# WebSocket proxy
location /ws/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Troubleshooting
Backend not starting
-
Check if port 3001 is available:
lsof -i :3001 -
Check backend logs in the terminal
-
Ensure Bun is installed:
bun --version
Drop backend not loading
- Check that
drops/<drop-name>/backend/exists - Check that
api.jshas a validexport const routes = {} - Look for errors in terminal output during startup
Database errors
- Check that
backend/db/directory exists - Check file permissions
- Review schema.sql for syntax errors
- Delete
braindrops.dbto start fresh (will lose data)
Development Tips
-
Route Patterns - Use Express-style patterns:
GET /items- List allGET /items/:id- Get by IDPOST /items- CreatePUT /items/:id- UpdateDELETE /items/:id- Delete
-
Error Handling - Return proper status codes:
if (!item) { return Response.json({ error: 'Not found' }, { status: 404 }) } -
Validation - Always validate input:
if (!body.name || !body.data) { return Response.json({ error: 'Missing fields' }, { status: 400 }) } -
JSON Storage - Store complex data as JSON:
run('INSERT INTO items (data) VALUES (?)', [JSON.stringify(obj)]) -
WebSocket Channels - Use drop name for pub/sub:
ws.publish('mydrop', JSON.stringify({ event: 'update' }))
License
Same as parent project.