braindrops.nicwands.com/BACKEND_GUIDE.md
2026-07-23 15:27:12 -04:00

11 KiB

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

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:

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:

-- 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:

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:

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):

// 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

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:

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:

    cp .env.example .env
    
  2. Add your variables:

    PRODUCTION_URL=https://yourdomain.com
    OPENAI_API_KEY=sk-...
    WEATHER_API_KEY=...
    
  3. Access in your backend code:

    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:

// 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:

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

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

# 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

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:

cd /var/www/braindrops/backend
nano .env

Add:

NODE_ENV=production
PRODUCTION_URL=https://yourdomain.com
# Your API keys...

6. Run Backend with Process Manager

Using PM2:

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:

bun --version

Install if needed:

curl -fsSL https://bun.sh/install | bash

Port already in use

Change the port in .env:

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!