This guide explains how to run the OpenMeet Platform (frontend) locally for development.
- Node.js v22+ (required by matrix-js-sdk)
- npm (>= 6.13.4)
- Docker & Docker Compose (optional, for production-like setup)
The simplest way to get started. Works with local API or remote dev API.
# 1. Install dependencies
npm install
# 2. Copy environment files
cp .env.example .env
cp public/config.example.json public/config.json
# 3. Start the dev server
npm run dev
# Opens browser at http://localhost:9005The default config points to http://localhost:3000 (local API) with tenant ID testing.
To use the production API with the test tenant (recommended for UI work — no local backend needed), edit public/config.json:
{
"APP_API_URL": "https://api.openmeet.net",
"APP_TENANT_ID": "oiupsdknasfdf",
"APP_TENANT_NAME": "OpenMeet Test",
"APP_TENANT_DESCRIPTION": "Test tenant for UI development",
"APP_TENANT_IMAGE": "/openmeet/openmeet-logo.png",
"NODE_ENV": "development"
}This connects to the real production API but uses an isolated test tenant — your data won't affect real users. You can register a new account, log in with Bluesky OAuth, create events, RSVP, and test the full UI.
Runs with nginx reverse proxy (closer to production).
# 1. Install dependencies first (prevents Docker permission issues)
npm install
# 2. Copy environment files
cp .env.example .env
cp public/config.example.json public/config.json
# 3. Create the shared network (if not already created by API)
docker network create api-network 2>/dev/null || true
# 4. Start platform with Docker
docker compose -f docker-compose-dev.yml up
# Access via http://localhost:9005To use production API with test tenant (no local API needed):
BACKEND_DOMAIN=https://api.openmeet.net docker compose -f docker-compose-dev.yml upNote: You may see
Error: spawn xdg-open ENOENTin the logs - this is harmless. The container is trying to open a browser, which isn't possible in Docker.
The platform uses two configuration mechanisms:
| File | Purpose | When Used |
|---|---|---|
.env |
Dev server settings, test credentials | Build-time, testing |
public/config.json |
API URL, tenant ID, OAuth | Runtime (fetched by browser) |
The browser fetches this at runtime. Default values work out of the box with local API:
{
"APP_API_URL": "http://localhost:3000",
"APP_TENANT_ID": "testing",
"APP_TENANT_NAME": "OpenMeet Dev",
"APP_TENANT_DESCRIPTION": "Development environment",
"NODE_ENV": "development"
}Important: The browser calls the API directly, so always use
localhost:3000(not Docker hostnames likeapi:3000).
For production deployments, see public/config.production.example.json which includes additional options:
- OAuth providers (Google, GitHub)
- Analytics (PostHog, HubSpot)
- Matrix chat integration
- Support/billing links
Default values work out of the box:
# Dev server
APP_DEV_SERVER_OPEN=true # Opens browser automatically
APP_DEV_SERVER_PORT=9005
# Tenant (must match public/config.json)
APP_TENANT_ID=testing
# Testing credentials (for Cypress e2e tests)
APP_TESTING_USER_EMAIL=test@openmeet.net
APP_TESTING_USER_PASSWORD=your-test-password
APP_TESTING_API_URL=http://localhost:3000| Command | Description |
|---|---|
npm run dev |
Start Quasar dev server with hot reload |
npm run build |
Production build |
npm run lint |
Run ESLint |
npm run lint -- --fix |
Fix ESLint issues |
npx vue-tsc --noEmit |
TypeScript type check |
# Unit tests (recommended - these are reliable)
npm run test:unit:ci # Run all unit tests
npm run test:unit # Watch mode
npx vitest run path/to/spec.ts # Run specific test
# E2E tests (⚠️ many are outdated)
npm run test:e2e # Interactive Cypress
npm run test:e2e:ci # Headless CI modeNote: Cypress E2E tests are currently out of date and will mostly fail. Unit tests are reliable. We'd love help updating the E2E tests!
The docker-compose-dev.yml runs two services:
| Service | Description |
|---|---|
platform-dev |
Quasar dev server (internal, port 9005) |
platform-nginx |
Nginx reverse proxy with bot detection |
Override BACKEND_DOMAIN to route nginx to different API backends:
# Default: Local API in Docker (requires openmeet-api running)
docker compose -f docker-compose-dev.yml up
# Production API with test tenant (no local API needed)
BACKEND_DOMAIN=https://api.openmeet.net docker compose -f docker-compose-dev.yml up
# Local API on host via npm (API running with `npm run start:dev`)
BACKEND_DOMAIN=http://host.docker.internal:3000 docker compose -f docker-compose-dev.yml upThe Docker setup uses a shared api-network. Create it if not already created by openmeet-api:
docker network create api-networkThis error is harmless - the container tries to open a browser (due to APP_DEV_SERVER_OPEN=true) but can't. The dev server still works fine at http://localhost:9005.
Create the shared network:
docker network create api-networkOr just use npm run dev instead (no Docker needed).
If changes aren't reflected:
- Check the terminal for compile errors
- Try hard refresh (Ctrl+Shift+R)
- Clear browser cache and localStorage
Use vue-tsc for type checking (there's no npm run typecheck):
npx vue-tsc --noEmitUse storeToRefs() for reactive store properties:
import { storeToRefs } from 'pinia'
const authStore = useAuthStore()
const { user, token } = storeToRefs(authStore) // ✅ Reactive
// const { user } = authStore // ❌ Not reactive# Find what's using the port
lsof -i :9005
# Kill the process or use a different port
APP_DEV_SERVER_PORT=9006 npm run devsrc/
├── pages/ # Route pages (auto-registered)
├── components/ # Reusable Vue components
├── stores/ # Pinia state stores
├── composables/ # Vue composition utilities
├── api/ # API client functions
├── types/ # TypeScript interfaces
├── router/ # Vue Router configuration
├── boot/ # Quasar boot files (config, axios, auth)
└── utils/ # Helper functions
public/
└── config.json # Runtime configuration (fetched by browser)
test/
├── vitest/ # Unit tests
└── cypress/ # E2E and component tests
The platform can be built as a native Android app using Capacitor.
Android Studio - Download from developer.android.com/studio
Android Studio includes a bundled JRE (JBR - JetBrains Runtime) with Java 21, so no separate Java installation is needed.
# 1. Set JAVA_HOME to Android Studio's bundled JRE
export JAVA_HOME=~/android-studio/jbr
# 2. Build the Quasar app for Android
npx quasar build -m capacitor -T android
# 3. Install to connected device (USB debugging enabled)
cd src-capacitor/android
./gradlew installDebug# Build web assets and open in Android Studio
npx quasar build -m capacitor -T android --ideFrom Android Studio, click the green play button to run on an emulator or connected device.
# List connected devices
~/Android/Sdk/platform-tools/adb devices -l
# Launch the app
~/Android/Sdk/platform-tools/adb shell monkey -p net.openmeet.platform -c android.intent.category.LAUNCHER 1
# View app logs (useful for debugging)
~/Android/Sdk/platform-tools/adb logcat | grep -i capacitor
# Pull screenshots from device
~/Android/Sdk/platform-tools/adb pull /sdcard/Pictures/Screenshots/ ./screenshots/
# Clear app data (reset to fresh state)
~/Android/Sdk/platform-tools/adb shell pm clear net.openmeet.platform"SDK location not found" - Create src-capacitor/android/local.properties:
sdk.dir=/home/YOUR_USERNAME/Android/Sdk
"JAVA_HOME is set to an invalid directory" - Use absolute path, not ~:
export JAVA_HOME=/home/YOUR_USERNAME/android-studio/jbr"Security Manager not supported" - Your system Java is too new. Use Android Studio's bundled JRE instead (see above).
Build a signed AAB for Google Play Store release.
- Android Studio with bundled JRE (Java 21)
- Keystore configured in
src-capacitor/android/keystore.properties - Google Play Console access for upload
# 1. Set JAVA_HOME to Android Studio's bundled JRE
export JAVA_HOME=~/android-studio/jbr
# 2. Switch config to production API
cp public/config.mobile.json public/config.json
# Verify it points to prod (not dev):
cat public/config.json | grep APP_API_URL
# Should show: "APP_API_URL": "https://api.openmeet.net"
# 3. Build web assets for Android
npx quasar build -m capacitor -T android
# 4. Build signed AAB
cd src-capacitor/android
./gradlew bundleRelease
# 5. Locate the signed AAB
ls -la app/build/outputs/bundle/release/app-release.aab- Go to Google Play Console
- Select OpenMeet app
- Navigate to Release → Production (or Testing track)
- Click Create new release
- Upload
app-release.aab - Add release notes
- Review and roll out
Update version in src-capacitor/android/app/build.gradle before each release:
defaultConfig {
versionCode 3 // Increment for each upload (must be unique)
versionName "1.0.2" // User-visible version
}| File | Points To | Use For |
|---|---|---|
public/config.json |
Current active config | Runtime |
public/config.mobile.json |
api.openmeet.net (prod) |
Production releases |
public/config.example.json |
localhost:3000 |
Local development |
After building, restore dev config if continuing development:
cp public/config.example.json public/config.json"Keystore not found" - Check src-capacitor/android/keystore.properties has correct absolute path to your keystore:
storeFile=/path/to/your/android-app-signing.jks"Unsigned APK" - Ensure keystore.properties exists and has valid credentials.
Build fails with memory error - Increase Gradle heap in gradle.properties:
org.gradle.jvmargs=-Xmx2048m- Platform: https://platform.openmeet.net
- API Docs: https://api.openmeet.net/docs
- Quasar Docs: https://quasar.dev
- Vue 3 Docs: https://vuejs.org
- Pinia Docs: https://pinia.vuejs.org