DEVELOPER DOCUMENTATION

ONLINE
PEDALBOARD

// full-stack browser-based guitar pedalboard + IR library

A web-based guitar pedalboard that lets musicians upload, browse, and audition Impulse Responses in real time — no software installation required. Built as a full-stack portfolio project demonstrating low-latency in-browser audio processing, containerised infrastructure, and automated CI/CD pipelines.

Vue 3 Web Audio API AudioWorklets FastAPI PostgreSQL MinIO Docker Compose GitHub Actions Vercel
01 / OVERVIEW

PROJECT OVERVIEW

Modern guitarists widely adopt Impulse Responses for cabinet simulation, yet there is no seamless way to audition them without first downloading files and loading them into a DAW or hardware pedalboard. Online Pedalboard eliminates that friction by running a live signal chain directly in the browser alongside a shared community IR library.

What the application does

Tech stack at a glance

LayerTechnologyHosting
FrontendVue 3 + Vite, Web Audio API, AudioWorkletsVercel
BackendFastAPI (Python 3.12), PostgreSQL, MinIO, NginxVPS via Docker
CI/CDGitHub Actions (lint → test → deploy)
EmailResend (transactional, password reset)
02 / ARCHITECTURE

SYSTEM ARCHITECTURE

The project follows a strict client/server split. The frontend is a purely static SPA that handles all real-time audio processing client-side. The backend is stateless at the application layer — all state lives in PostgreSQL and MinIO — and is the sole authority for authentication and IR storage.

BrowserVercel CDN (Vue 3 SPA)

Browser ←→ Nginx (HTTPS : 443, VPS)
  ├── /api/* → FastAPI container (internal)
  └── /files/* → MinIO container (internal)

FastAPI ←→ PostgreSQL (users, IR metadata, favourites)
FastAPI ←→ MinIO (.wav object storage)

All backend containers share an internal Docker bridge: pedalboard-net

Repository structure

monorepo
online-pedalboard/
├── frontend/                   # Vue 3 SPA → Vercel
│   └── src/
│       ├── PedalBoard.vue      # root orchestrator
│       ├── views/              # route-level views
│       ├── components/
│       │   ├── CabinetPedal.vue   # IR loader & convolver UI
│       │   ├── EffectPedal.vue    # delay / reverb UI
│       │   └── PreampPedal.vue   # distortion & EQ UI
│       ├── composables/
│       │   ├── useAudioEngine.js  # ← Web Audio API core
│       │   └── useIRLibrary.js    # ← IR fetch & state
│       └── router/index.js
│
└── backend/                    # FastAPI → VPS (Docker)
    ├── app/
    │   ├── main.py             # app factory & middleware
    │   ├── config.py           # Pydantic settings
    │   ├── database.py         # SQLAlchemy engine
    │   ├── dependencies.py     # auth dependency injection
    │   ├── models/             # ORM models
    │   └── routers/
    │       ├── auth.py         # /auth endpoints
    │       └── irs.py          # /irs endpoints
    ├── nginx/nginx.conf
    ├── Dockerfile
    ├── docker-compose.yml
    └── tests/

Frontend architecture

The frontend is a Vue 3 SPA built with Vite. All audio processing runs client-side; the server is never in the audio path. Two composables carry the core logic:

Backend architecture

FastAPI runs under Uvicorn, containerised with Docker. Four layers:

03 / AUDIO ENGINE

AUDIO ENGINE

The audio engine runs entirely in the browser using the Web Audio API. The design goal is the minimum achievable latency while keeping the main (UI) thread free of audio computation.

AudioWorklets

Standard ScriptProcessorNode runs on the main thread and is deprecated. The pedalboard uses AudioWorklets, which execute in a dedicated audio rendering thread. This eliminates garbage-collection pauses and UI jank that would otherwise cause audible dropouts.

⚠ Known limitation

Web Audio API latency is bounded by the browser's audio buffer size (typically 128–512 frames at 44.1 kHz, i.e. ~3–12 ms of processing latency). This cannot be reduced below what the browser exposes. AudioWorklets bring it as low as the browser allows, but some buffer delay is unavoidable.

Signal chain

Audio flows through the following ordered nodes. Each stage is independently bypassable via a toggle in the UI:

🎸 Input
Captures the guitar signal from the browser's audio input via getUserMedia(). The resulting MediaStream is wrapped in a MediaStreamAudioSourceNode and connected to the first processing node.
🔥 Distortion WaveShaperNode + GainNode
A WaveShaperNode applies a non-linear transfer curve to the signal. The curve is computed from a drive parameter (0–100). Higher drive produces heavier clipping. Pre-gain is applied via a GainNode before the shaper.
🎛 EQ BiquadFilterNode ×3
Three BiquadFilterNodes in series: a low-shelf (bass), a peaking filter (mid), and a high-shelf (treble). Frequency, gain, and Q values are bound directly to AudioParam objects via Vue reactive refs, enabling sample-accurate automation.
📦 Cabinet IR ConvolverNode
A ConvolverNode performs real-time convolution with the loaded Impulse Response buffer. When the user picks an IR, useIRLibrary fetches the .wav, CabinetPedal decodes it via AudioContext.decodeAudioData(), and sets ConvolverNode.buffer. The node is normalised to prevent clipping. See IR Loading Flow below.
Delay DelayNode + GainNode
A DelayNode feeds into a feedback GainNode that routes back to the delay input, creating echoes. Time (0–2 s) and feedback (0–0.9) are user-controllable. A wet/dry GainNode blends the processed and dry paths.
🌊 Reverb ConvolverNode
A second ConvolverNode loaded with a static plate-reverb impulse response that ships with the frontend bundle. A wet/dry GainNode blend controls the perceived room size.
🔊 Output GainNode + AnalyserNode
A master-volume GainNode connects to AudioContext.destination. A parallel

IR loading flow

  1. User selects an IR from the library (useIRLibrary provides metadata including the .wav URL).
  2. CabinetPedal calls fetch() on the presigned MinIO URL to retrieve the raw ArrayBuffer.
  3. AudioContext.decodeAudioData(arrayBuffer) decodes the .wav into an AudioBuffer on the audio thread.
  4. ConvolverNode.buffer is set to the decoded AudioBuffer — old convolution is replaced immediately.
  5. A bypass GainNode is set to 0 during the brief decode window, then restored to 1 to avoid audible glitches.

Latency breakdown

SourceTypical RangeNotes
Input buffer5–20 msOS audio driver + browser implementation.
Audio graph processing~10 msOne rendering quantum (128 frames ≈ 2.9 ms) per node.
Output buffer10–30 msBrowser output buffer size.
Total20–60 msVaries by hardware and browser. Within acceptable monitoring range.
04 / API REFERENCE

API REFERENCE

The backend exposes a JSON REST API via FastAPI, served through Nginx at /api/*. Authentication uses JWT Bearer tokens issued by POST /auth/login and signed with HS256.

🔐 Authentication

Protected endpoints require an Authorization: Bearer <token> header. Token expiry is enforced server-side. Tokens are issued only by POST /auth/login.

Authentication endpoints

Router: backend/app/routers/auth.py

MethodEndpointAuthDescription
POST /auth/register None Register a new user account. Body: { email, password, username }.
POST /auth/login None Authenticate and receive a JWT access token. Body: OAuth2 form (username, password).
POST /auth/forgot-password None Trigger a password-reset email via Resend. Body: { email }.
POST /auth/reset-password None Submit a new password with the reset token received by email. Body: { token, new_password }.
GET /auth/me Bearer Return the currently authenticated user's profile.
PUT /auth/me Bearer Update account details (username, email). Body: partial user object.

IR library endpoints

Router: backend/app/routers/irs.py

MethodEndpointAuthDescription
GET /irs/ None List all IRs. Query params: ?search=, ?sort=, ?page=, ?per_page=.
POST /irs/ Bearer Upload a new IR. Multipart form: file (.wav), name, description. Stores file in MinIO, records metadata in PostgreSQL.
GET /irs/{ir_id} None Retrieve metadata for a single IR: name, description, uploader, download count, created_at.
DELETE /irs/{ir_id} Bearer Delete an IR. Only the uploader or an admin may delete. Also removes the file from MinIO.
GET /irs/{ir_id}/file None Stream or redirect to the .wav file in MinIO (presigned URL or direct proxy).
POST /irs/{ir_id}/favourite Bearer Toggle favourite status for the authenticated user on the given IR.
GET /irs/favourites Bearer List all IRs favourited by the current user.
GET /irs/{ir_id}/stats Bearer Return usage statistics: download count, unique listeners, trend over time.
05 / DATABASE

DATABASE SCHEMA

PostgreSQL is used as the primary data store. The schema consists of four tables managed via SQLAlchemy ORM models in backend/app/models/. All primary keys are auto-incrementing integers; timestamps are stored as UTC.

users auth
PK id INTEGER auto-increment
username VARCHAR(64) unique, not null
email VARCHAR(255) unique, not null
hashed_password TEXT bcrypt
reset_token TEXT nullable
reset_token_expires TIMESTAMP nullable, UTC
created_at TIMESTAMP default now()
impulse_responses ir library
PK id INTEGER auto-increment
FK uploader_id INTEGER → users.id
name VARCHAR(128) not null
description TEXT nullable
storage_key TEXT MinIO object key
file_size_bytes INTEGER nullable
download_count INTEGER default 0
created_at TIMESTAMP default now()
favourites junction
PK id INTEGER auto-increment
FK user_id INTEGER → users.id
FK ir_id INTEGER → impulse_responses.id
created_at TIMESTAMP default now()
usage_events analytics
PK id INTEGER auto-increment
FK ir_id INTEGER → impulse_responses.id
FK user_id INTEGER → users.id, nullable
event_type VARCHAR(32) e.g. "download"
occurred_at TIMESTAMP default now()

Relationships

users ──( 1 : N )──› impulse_responses A user can upload many IRs.
users ──( M : N )──› impulse_responses Via favourites junction table.
impulse_responses ──( 1 : N )──› usage_events Each IR can have many usage events.
users ──( 1 : N )──› usage_events Nullable — anonymous listens are also tracked.

Notes

06 / Lint and Testing

TESTING

Both the frontend and backend have independent test suites that run automatically on every push via GitHub Actions. All tests must pass before a deployment to main is triggered.

Backend — pytest

Located in backend/tests/. The suite runs against a live ephemeral PostgreSQL instance spun up as a GitHub Actions service container, ensuring tests exercise real database behaviour rather than mocks.

🔐
Auth — registration & login
Verifies that a new user can register with valid credentials, that duplicate emails are rejected with a 409, and that a successful login returns a well-formed JWT token.
🔑
Auth — password reset flow
Covers the full reset lifecycle: requesting a reset token, submitting a new password with a valid token, and confirming that expired or invalid tokens are rejected with a 400.
🛡️
Auth — protected route enforcement
Confirms that endpoints requiring a Bearer token return 401 when called without one, and 403 when called with a token belonging to a non-owner user (e.g. deleting another user's IR).
📦
IR — upload & metadata
Tests that uploading a .wav file creates the expected record in PostgreSQL and that the returned metadata (name, description, uploader, storage key) matches the submitted form data.
📋
IR — listing & filtering
Checks that GET /irs/ returns the correct paginated results, that the ?search= query param filters by name, and that ?sort= orders results as expected.
❤️
IR — favourites toggle
Verifies idempotent toggling: a first POST /irs/{id}/favourite inserts a row in favourites, a second call removes it, and the GET /irs/favourites list reflects the state correctly.
🗑️
IR — deletion & ownership
Asserts that only the uploader (or an admin) can delete an IR, and that a successful delete removes both the PostgreSQL record and the MinIO object.
run backend tests
cd backend
pytest           # Uses a dummy sqlLite instance for testing

Frontend — Vitest

Located in frontend/src/components/__tests__/ and frontend/src/composables/__tests__/. Tests run in a jsdom environment and use Vue Test Utils to mount components in isolation.

🎛️
useAudioEngine — graph construction
Verifies that calling the composable creates the expected Web Audio nodes (WaveShaperNode, BiquadFilterNodes, ConvolverNode, DelayNode) and that they are connected in the correct order. Uses a mocked AudioContext.
🎚️
useAudioEngine — effect parameter binding
Checks that updating a reactive control (e.g. drive, EQ gain) propagates to the corresponding AudioParam on the underlying node.
📚
useIRLibrary — fetch & state
Mocks the REST API and confirms that the composable correctly populates the reactive IR list, handles loading and error states, and updates the selected IR reference when a new one is chosen.
🖼️
Component rendering
Smoke tests for PreampPedal, CabinetPedal, and EffectPedal — assert that each component mounts without errors and renders its expected UI elements (knobs, toggles, labels).
run frontend tests
cd frontend
npx test            # run once

Lint checks

Linting runs on every push to any branch — not just main — so code quality is enforced before a PR is even opened.

Frontend — ESLint eslint
Uses the Vue 3 recommended ruleset, catching common mistakes like missing key attributes in v-for, misused v-model bindings, and unreachable template code.
Enforces consistent code style: no unused variables, no console.log left in production code, and consistent component naming conventions.
Backend — Ruff ruff check
Flags unused imports and variables — particularly important in FastAPI where unused dependencies can silently inflate request overhead.
Enforces PEP 8 style conventions and catches common anti-patterns such as mutable default arguments and bare except clauses.
Ruff is configured to be fast enough to run on every push without slowing CI meaningfully.

Test Coverage

Backend (pytest-cov)
94%
Frontend (Vitest)
77.51%
07 / DEPLOYMENT

DEPLOYMENT & CI/CD

Infrastructure

Two hosting environments, four Docker containers:

nginx :80 :443 (public)

Sole public entry point. Handles SSL termination (Let's Encrypt), routes /api/* → FastAPI and /files/* → MinIO.

backend internal only

FastAPI + Uvicorn. Serves the REST API. Never directly reachable from the internet.

postgres internal only

PostgreSQL 15. Stores user accounts, IR metadata, favourites, and usage events.

minio internal only

S3-compatible object storage for .wav IR files. Accessed by the backend and proxied via Nginx /files/*.

🔒 SSL

Certificates are issued by Let's Encrypt via Certbot and auto-renewed by a daily cron job on the VPS host.

Backend CI/CD pipeline

Triggered on every push. Deploy steps run only on main.

01
Lint — Ruff
Checks for unused variables, imports, and code-quality violations. Fails fast.
02
Test — pytest
Runs test suite against an ephemeral PostgreSQL service container spun up inside the Actions runner.
03
Deploy — SSH to VPS (main only)
SSHs into the VPS using a key stored in GitHub Secrets; SCPs the backend/ folder.
04
Write .env from Secrets (main only)
Writes a fresh .env from GitHub Secrets directly onto the server. Secrets never touch git.
05
docker compose up --build (main only)
Rebuilds and restarts all containers. The VPS never runs git pull — it just receives files and runs Docker.

Frontend CI/CD pipeline

01
Lint — ESLint
Enforces Vue 3 recommended code-quality standards.
02
Test — Vitest
Runs the frontend test suite.
03
Deploy — Vercel (main only)
Vercel detects the push and triggers its own Vite build + CDN deploy automatically.

Required GitHub Secrets

VPS_HOST
IP address or domain of the VPS.
VPS_USER
SSH username on the VPS.
VPS_SSH_KEY
Private SSH key (public key added to VPS).
SECRET_KEY
JWT signing secret — 32+ random bytes.
DATABASE_URL
Full PostgreSQL connection string.
POSTGRES_USER
PostgreSQL superuser username.
POSTGRES_PASSWORD
PostgreSQL superuser password.
POSTGRES_DB
Database name to create/use.
MINIO_ROOT_USER
MinIO root/admin username.
MINIO_ROOT_PASSWORD
MinIO root/admin password.
MINIO_ACCESS_KEY
MinIO access key for the FastAPI client.
MINIO_SECRET_KEY
MinIO secret key for the FastAPI client.
RESEND_API_KEY
Resend API key. Leave empty to disable email locally.
FRONTEND_URL
Vercel URL used for CORS allowlist in FastAPI.
08 / LOCAL SETUP

LOCAL SETUP

Prerequisites

Frontend

terminal
cd frontend
npm install
cp .env.example .env   # set VITE_API_URL=http://localhost:8000
npm run dev            # → http://localhost:5173

If VITE_API_URL is not set, the SPA falls back to http://localhost:8000 automatically.

Backend — local Python (recommended for dev)

Run only PostgreSQL and MinIO in Docker; run FastAPI directly for fast hot-reload iteration:

terminal
cd backend
python3.12 -m venv venv
# Windows:  .\venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload

Backend — full Docker Compose

terminal
cd backend
cp .env.example .env   # fill in all required values
docker compose up -d   # starts all 4 containers

Environment variables (backend)

SECRET_KEYJWT signing secret (32+ random bytes).
DATABASE_URLpostgresql://user:pass@postgres:5432/pedalboard
POSTGRES_USERPostgreSQL username.
POSTGRES_PASSWORDPostgreSQL password.
POSTGRES_DBDatabase name.
MINIO_ENDPOINTminio:9000 (Docker Compose internal hostname).
MINIO_ACCESS_KEYMinIO access key.
MINIO_SECRET_KEYMinIO secret key.
MINIO_BUCKETBucket name for IR files.
MINIO_PUBLIC_URLPublic URL for MinIO (used to build download links).
RESEND_API_KEYLeave empty to disable outbound email locally.
FRONTEND_URLhttp://localhost:5173 for local development.

Running tests

backend
cd backend
pip install -r requirements.txt
pytest tests/   # requires a running PostgreSQL instance
frontend
cd frontend
npm run test    # runs Vitest
09 / CONTRIBUTING

CONTRIBUTING

  1. Open an issue first before submitting a PR for anything beyond small bug fixes — discuss the approach first.
  2. Fork the repository and create a feature branch from main.
  3. Ensure linting passes: run eslint (frontend) and ruff check (backend) locally.
  4. Write or update tests for any changed behaviour.
  5. Open a pull request against main with a clear description of what changed and why.

Code quality tools

Known limitations

↑ TOP