braindrops.nicwands.com/backend
2026-07-23 15:27:12 -04:00
..
src backend integration 2026-07-23 15:27:12 -04:00
.gitignore backend integration 2026-07-23 15:27:12 -04:00
bun.lockb backend integration 2026-07-23 15:27:12 -04:00
package.json backend integration 2026-07-23 15:27:12 -04:00
README.md backend integration 2026-07-23 15:27:12 -04:00

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

  1. Copy backend/dist/ to your server
  2. Install dependencies: cd backend/dist && bun install
  3. Set environment variables
  4. 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

  1. Check if port 3001 is available:

    lsof -i :3001
    
  2. Check backend logs in the terminal

  3. Ensure Bun is installed:

    bun --version
    

Drop backend not loading

  1. Check that drops/<drop-name>/backend/ exists
  2. Check that api.js has a valid export const routes = {}
  3. Look for errors in terminal output during startup

Database errors

  1. Check that backend/db/ directory exists
  2. Check file permissions
  3. Review schema.sql for syntax errors
  4. Delete braindrops.db to start fresh (will lose data)

Development Tips

  1. Route Patterns - Use Express-style patterns:

    • GET /items - List all
    • GET /items/:id - Get by ID
    • POST /items - Create
    • PUT /items/:id - Update
    • DELETE /items/:id - Delete
  2. Error Handling - Return proper status codes:

    if (!item) {
      return Response.json({ error: 'Not found' }, { status: 404 })
    }
    
  3. Validation - Always validate input:

    if (!body.name || !body.data) {
      return Response.json({ error: 'Missing fields' }, { status: 400 })
    }
    
  4. JSON Storage - Store complex data as JSON:

    run('INSERT INTO items (data) VALUES (?)', [JSON.stringify(obj)])
    
  5. WebSocket Channels - Use drop name for pub/sub:

    ws.publish('mydrop', JSON.stringify({ event: 'update' }))
    

License

Same as parent project.