A web application for managing karaoke song queues, rooms, playlists, and live session control. The backend is a Spring Boot service that integrates with the VoxBox platform (via the internal com.vpo:vbclient library) to drive song search, queueing, playback, lights, and on-screen popups for connected rooms. The frontend is an AngularJS / Ionic single-page app that DJs and hosts use from a phone or tablet.
Backend
- Java 21 (virtual threads enabled via
spring.threads.virtual.enabled=true), Spring Boot 3.5.x - Spring Web, Spring Security, Spring WebSocket, Spring Integration
- Spring AI MCP Server (Streamable HTTP)
- Spring Data MongoDB (domain persistence)
- Spring Session backed by Redis (
@EnableRedisHttpSessionin DjvbApplication.java) - Firebase Admin SDK (auth)
com.vpo:vbclient— internal VoxBox client, resolved from GitHub Packages (see Consuming vbclient below)
Frontend (djvb-ui/)
- AngularJS + Ionic + RequireJS
- Built and bundled with Grunt + Bower (build config and
package.json/bower.jsonlive insidedjvb-ui/) - Unit tests via Karma + Jasmine
Datastores
- MongoDB — domain data (users, queues, playlists, avatars, managers)
- Redis — HTTP session store
src/main/java/com/vpo/djvoxbox/
DjvbApplication.java Spring Boot entry point
app/ Services (QueueManagementService, UserService, UpdateService)
web/ REST controllers under /api/v1/* (queue, songs, playlists, avatar, user, ...)
domain/ Mongo documents + repositories (User, UserQueue, Playlists, Avatar, Manager)
config/ SecurityConfiguration, FirebaseConfig, SimpleCORSFilter, VoxBoxConfig
faye/ Faye/CometD subscription to VoxBox push events (replaces the old 25s poll)
mcp/ Hosted vbsongs MCP tools, OAuth endpoints, and operation wrappers
security/ Custom remember-me / session pieces
util/ Shared helpers (e.g. SessionUtils)
src/main/resources/
application.properties Production config
application-development.properties Local dev config
firebaseServiceAccountKey.json Firebase Admin credentials (do NOT commit real keys)
static/ Frontend bundle served by Spring Boot
djvb-ui/ AngularJS/Ionic frontend (sources, build pipeline, deps, tests)
Gruntfile.js Frontend build pipeline
package.json / bower.json Frontend tooling + bower deps
karma.conf.js Karma unit-test config
test/ Karma/Jasmine specs
docker-compose.yml Local MongoDB + Redis
Procfile Heroku-style run command
- Java 21 and Maven 3.x
- Docker / Docker Compose (for local MongoDB and Redis)
- Node.js ≥ 18 and npm — djvb-ui/package.json still pins
engines.nodeto6.11.1for legacy reasons; modern Node works for the tasks we use. Pass--legacy-peer-depstonpm installto tolerate the old dependency graph. - A Firebase service account JSON placed at src/main/resources/firebaseServiceAccountKey.json
Grunt and Bower CLIs are pulled in as local dev dependencies — no global install needed. Run them from inside djvb-ui/ via ./node_modules/.bin/grunt and ./node_modules/.bin/bower.
com.vpo:vbclient is published to GitHub Packages at maven.pkg.github.com/wordsandnumbers/vbclient. GitHub Packages requires authentication even for read, so every dev machine needs a one-time setup:
-
Create a GitHub PAT with
read:packagesscope at https://github.com/settings/tokens. -
Export it from your shell profile:
export GITHUB_PACKAGES_TOKEN=ghp_... -
Add a server entry to
~/.m2/settings.xml(create the file if it doesn't exist):<settings> <servers> <server> <id>github-vbclient</id> <username>YOUR_GITHUB_USERNAME</username> <password>${env.GITHUB_PACKAGES_TOKEN}</password> </server> </servers> </settings>
The <id> must match the repository id in pom.xml. Once configured, mvn resolves vbclient transparently.
-
Start MongoDB and Redis:
docker compose up -d
This brings up
mongo:6on27017andredis:7-alpineon6379(see docker-compose.yml). -
Install frontend dependencies (one-time):
cd djvb-ui npm install --legacy-peer-deps ./node_modules/.bin/bower install cd ..
npm installbrings in Grunt, the Bower CLI, and all the Grunt plugins listed in djvb-ui/package.json.bower installthen pulls the AngularJS / Ionic / Firebase frontend libraries intodjvb-ui/bower_components/per djvb-ui/bower.json. -
Run the backend against the
developmentprofile:SPRING_PROFILES_ACTIVE=development mvn spring-boot:run -DskipTests
The dev profile reads application-development.properties, which points at the local Mongo (
mongodb://localhost:27017/djvb) and Redis. The backend listens onhttp://localhost:8080. -
In a separate terminal, sync the UI into the backend's static-resources directory and watch for changes:
(cd djvb-ui && ./node_modules/.bin/grunt java)The
javatask (djvb-ui/Gruntfile.js):- cleans
target/classes/static/(at the project root) - runs
wiredepto inject Bower CSS@imports into djvb-ui/styles/main.css - copies
djvb-ui/bower_components/intotarget/classes/static/resources/bower_components/ - syncs
djvb-ui/intotarget/classes/static/resources/ - rewrites the RequireJS config in djvb-ui/scripts/main.js
- watches
djvb-ui/**and re-syncs on every change
Then open
http://localhost:8080/— it will redirect through the formLogin to resources/index.html once you're authenticated. Edits indjvb-ui/show up after a browser reload (no livereload wired into thejavatask). - cleans
Key properties (set in application.properties or application-development.properties, or overridden via environment / -D flags):
| Property | Purpose |
|---|---|
spring.data.mongodb.uri |
MongoDB connection string |
spring.data.redis.host / spring.data.redis.port / spring.data.redis.password |
Redis (session store) |
vb.organization |
VoxBox organization ID this instance is bound to |
manager.name |
Manager record name used to bootstrap state |
default.language |
Default song search language |
server.session.timeout |
HTTP session timeout (seconds) |
mcp.apiToken |
Optional static bearer token accepted by /mcp for compatibility and local smoke tests |
mcp.publicBaseUrl |
Public origin used in MCP OAuth metadata, for example https://djvb.example.com |
mcp.oauth.consentCode |
Shared human-entered authorization code used by the lightweight MCP OAuth consent page |
mcp.oauth.clientId / mcp.oauth.clientSecret |
Optional pre-registered OAuth client credentials |
mcp.oauth.authorizationCodeTtlSeconds / mcp.oauth.accessTokenTtlSeconds / mcp.oauth.refreshTokenTtlSeconds |
MCP OAuth token lifetimes |
The committed application.properties contains real-looking credentials. Rotate them and move them to environment variables / a secrets manager before deploying.
The backend hosts a vbsongs MCP server at /mcp using Spring AI's WebMVC Streamable HTTP transport. MCP tools delegate to com.vpo:vbclient and use this instance's configured vb.rootUrl, vb.organization, and default.language.
The server is intentionally client-neutral. Claude, Codex, or any other MCP client that supports remote Streamable HTTP plus OAuth discovery can connect to the same endpoint.
Authentication supports two paths:
- OAuth:
/mcpresponds to unauthenticated requests with aWWW-Authenticatechallenge pointing at/.well-known/oauth-protected-resource/mcp. The OAuth layer exposes protected-resource metadata, authorization-server metadata, dynamic client registration, authorization-code + PKCE, and refresh tokens. - Static bearer: clients may also call
/mcpwithAuthorization: Bearer <mcp.apiToken>. This is useful for local tests and simple internal clients.
For local development, application-development.properties sets both mcp.apiToken and mcp.oauth.consentCode to development-mcp-token.
Useful local checks:
curl -i http://localhost:8080/mcp
curl http://localhost:8080/.well-known/oauth-protected-resource/mcp
curl http://localhost:8080/.well-known/oauth-authorization-server
curl -i -H 'Authorization: Bearer development-mcp-token' http://localhost:8080/mcpThe last command is only an auth-boundary smoke test; use an MCP client for a real Streamable HTTP session.
For production, set:
MCP_PUBLIC_BASE_URL=https://your-djvb-host
MCP_OAUTH_CONSENT_CODE=<operator-shared-auth-code>
MCP_API_TOKEN=<optional-static-bearer-token>Current limitation: OAuth clients, authorization codes, access tokens, and refresh tokens are stored in memory. Restarting the server invalidates OAuth sessions, and multiple server instances would not share tokens without adding Redis/DB-backed storage or a full authorization server.
- Backend tests:
mvn test(entry point: src/test/java/com/vpo/djvoxbox/DjvbApplicationTests.java) - Backend jar:
mvn package -DskipTestsproducestarget/djvb-0.0.1-SNAPSHOT.jar - Frontend commands run from inside
djvb-ui/. The production build (gruntdefault task) andgrunt test(Karma + Jasmine) are currently not wired up for modern Node — they relied onnode-sassandphantomjs, which were dropped during the Node 24 / Spring Boot 3 upgrade. The dev loop (grunt java) is the supported workflow.
The whole stack runs as containers — server (Spring Boot) and UI (Caddy + static SPA) — alongside MongoDB and Redis. Local end-to-end mirrors what runs on ECS.
# server build needs a GitHub PAT with read:packages to fetch vbclient
export GITHUB_PACKAGES_TOKEN=ghp_...
DOCKER_BUILDKIT=1 docker compose up --buildOpen http://localhost/ — Caddy serves the SPA from /resources/* and reverse-proxies everything else (/, /api/*, /login*, /logout, /actuator/*) to the server. Health check: curl http://localhost/actuator/health.
Build images individually:
DOCKER_BUILDKIT=1 docker build -f Dockerfile.server \
--secret id=gh_pat,env=GITHUB_PACKAGES_TOKEN \
-t djvb-server:dev .
docker build -f Dockerfile.ui -t djvb-ui:dev .Production runs as four ECS services (caddy-ui, server, mongo, redis) on a single t4g.small EC2 instance in us-west-2, with a separate EBS data volume for Mongo and Redis persistence across instance replacement. Caddy terminates TLS with auto-issued Let's Encrypt certs — no ALB, no ACM. DNS lives at Cloudflare; an A record pointed at the EIP is the only thing AWS doesn't manage. Estimated steady-state cost ~$14/mo.
See infra/terraform/:
cd infra/terraform
cp terraform.tfvars.example terraform.tfvars # then edit if needed
terraform init
terraform applyAfter terraform apply completes, grab the EIP from the host_public_ip output and add an A record at Cloudflare:
- Name:
djvb(the subdomain part of yourdomain_name) - IPv4 address: the
host_public_ipoutput - Proxy status: DNS only (grey cloud, NOT orange/proxied) — Caddy needs direct access to the origin to complete the ACME HTTP-01 challenge.
Populate the SecureString secrets that Terraform left as placeholders:
aws ssm put-parameter --name /djvb/mongo_uri --type SecureString --overwrite --value 'mongodb://localhost:27017/djvb'
aws ssm put-parameter --name /djvb/redis_password --type SecureString --overwrite --value '<strong-redis-pw>'
aws ssm put-parameter --name /djvb/vb_organization --type SecureString --overwrite --value '<vb-org-id>'
aws ssm put-parameter --name /djvb/firebase_key --type SecureString --overwrite --value file://src/main/resources/firebaseServiceAccountKey.json
aws ssm put-parameter --name /djvb/mcp_oauth_consent_code --type SecureString --overwrite --value '<strong-mcp-operator-code>'MCP_PUBLIC_BASE_URL is set automatically in the ECS server task from domain_name as https://<domain_name>.
Then force a redeploy so the tasks pick up the new secret values:
aws ecs update-service --cluster djvb --service djvb-server --force-new-deployment
aws ecs update-service --cluster djvb --service djvb-ui --force-new-deploymentCI/CD is wired up in .github/workflows/deploy.yml: pushes to main build both images for linux/arm64, push to ECR with both <sha> and latest tags, then register a new ECS task-definition revision pinned to the <sha> tag and update each service to it (via .github/scripts/deploy-service.sh). The only repo secret needed is AWS_DEPLOY_ROLE (output by Terraform as github_deploy_role_arn); the workflow fetches vbclient via the auto-provided GITHUB_TOKEN with packages: read permission, so no PAT is stored in GitHub.
For GITHUB_TOKEN to read the vbclient package, that package must grant access to this repo: in the vbclient package settings under Manage Actions access, add wordsandnumbers/djvb with Read role.
Every deploy registers a new ECS task-definition revision pinned to its git SHA, so prior revisions remain valid rollback targets. ECR keeps the last 10 image revisions, so any of the last ~10 deployed SHAs are available.
In order of preference:
-
Manual rollback workflow — re-pins both services to an existing SHA already in ECR (no rebuild, ~1 min):
# find a recent good SHA gh run list --workflow=deploy.yml --branch=main --limit 10 gh workflow run rollback.yml -f sha=<good-sha>
Optionally pass
-f services=djvb-serverto roll back only one service. See .github/workflows/rollback.yml. -
Revert the commit on
main— slower (rebuilds), but keepsmainHEAD = what's running:git revert <bad-sha> && git push
-
Auto-rollback (passive) —
djvb-serveranddjvb-uihave the ECS deployment circuit breaker enabled withrollback = true(infra/terraform/ecs.tf), so a deploy whose tasks fail to start will auto-restore the previous task-def revision after ~5–15 min without operator action.
Note: rollback only restores the container images, not MongoDB data. There are no scheduled Mongo snapshots — a data-corrupting bug is not recoverable by these steps.