Skip to main content
YesPaPa provides a free default remote server, but you can self-host your own for maximum control and security. The remote server is only needed for the Outer Ring (mobile app push notifications and one-tap approvals). The Inner Ring (TOTP gate) works fully offline. This guide covers two options:
  • Option 1: Docker — A standalone Node.js server you run yourself. Recommended for self-hosting.
  • Option 2: Supabase — The hosted cloud option (or self-hosted Supabase). This is the current default.

The server/ directory contains a standalone YesPaPa remote server built with Express, WebSocket, SQLite, and JWT authentication. It implements the full remote server protocol and is the reference self-hosted implementation.

Prerequisites

Quick Start

Pull the image from DockerHub and run it:
docker run -d \
  --name yespapa-server \
  -p 8080:8080 \
  -e JWT_SECRET=$(openssl rand -base64 32) \
  -v yespapa-data:/app/data \
  --restart unless-stopped \
  skyaiops/yespapa-server:latest
Or use Docker Compose. Create a docker-compose.yml:
services:
  yespapa-server:
    image: skyaiops/yespapa-server:latest
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
      - JWT_SECRET=change-me-to-a-random-string
      - DATABASE_PATH=/app/data/yespapa.db
      - EXPO_PUSH_ENABLED=false
    volumes:
      - yespapa-data:/app/data
    restart: unless-stopped

volumes:
  yespapa-data:
Then start it:
docker compose up -d
The server starts on port 8080 by default. Verify it is running:
curl http://localhost:8080/health
# Expected: {"status":"ok","version":"0.1.0"}

Configuration

Environment variables are set in docker-compose.yml:
VariableDefaultDescription
PORT8080Port the server listens on
JWT_SECRETchange-me-to-a-random-stringMust be changed. Secret used to sign JWT tokens. Use a long random string (32+ characters).
DATABASE_PATH/app/data/yespapa.dbPath to the SQLite database inside the container
EXPO_PUSH_ENABLEDfalseSet to true to enable push notifications via Expo Push API
Generate a strong JWT secret:
openssl rand -base64 32
Then update docker-compose.yml:
environment:
  - JWT_SECRET=your-generated-secret-here

Data Persistence

All data is stored in a Docker volume named yespapa-data, mapped to /app/data inside the container. This volume persists across container restarts and rebuilds. The SQLite database (yespapa.db) lives in this volume. To back up your data:
docker compose cp yespapa-server:/app/data/yespapa.db ./yespapa-backup.db

Connecting YesPaPa to Your Docker Server

During yespapa init, provide your server URL and select the selfhosted server type:
Remote server URL: http://your-server-ip:8080
Remote server type: selfhosted
If your server is behind a reverse proxy with TLS, use the https:// URL instead. If already initialized, you can reinitialize:
yespapa uninstall && yespapa init

API Endpoints

The Docker server exposes these endpoints:
EndpointDescription
GET /healthHealth check, returns server status and version
/api/auth/*Authentication (register, login, token refresh)
/api/hosts/*Host registration and management
/api/commands/*Command approval queue (create, list, approve, deny)
/api/grace-periods/*Grace period management
/api/push/*Push notification registration
/wsWebSocket endpoint for real-time command and grace period updates

Push Notifications

Push notifications are disabled by default. To enable them:
  1. Set EXPO_PUSH_ENABLED=true in docker-compose.yml
  2. Restart the container: docker compose restart
Push notifications use the Expo Push API, so the server needs outbound internet access to reach https://exp.host/--/api/v2/push/send.

Running Without Docker

You can also run the server directly with Node.js (v18+):
cd server
npm install
npm run build
JWT_SECRET=your-secret-here npm start

Updating

To update to a newer version:
docker pull skyaiops/yespapa-server:latest
docker compose up -d
Or pin a specific version:
docker pull skyaiops/yespapa-server:0.2.0
# Update the image tag in docker-compose.yml, then:
docker compose up -d

Docker Troubleshooting

Container fails to start

  • Check logs: docker compose logs yespapa-server
  • Ensure port 8080 is not in use: lsof -i :8080
  • Verify the Docker volume exists: docker volume ls | grep yespapa-data

”Connection refused” from YesPaPa daemon

  • Ensure the server is running: docker compose ps
  • If the daemon is on the same machine, use http://localhost:8080 or http://host.docker.internal:8080
  • If on a different machine, ensure the server port is accessible (check firewalls)
  • Verify with: curl http://your-server-ip:8080/health

Database errors or corruption

  • Stop the server: docker compose down
  • Back up the current database from the volume
  • Remove the volume to start fresh: docker volume rm server_yespapa-data
  • Restart: docker compose up -d

WebSocket connection drops

  • If behind a reverse proxy (nginx, Caddy), ensure WebSocket upgrade is configured
  • For nginx, add to your location block:
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    

Option 2: Supabase (Hosted / Cloud)

Supabase is the current default remote backend. You can use the hosted Supabase platform (free tier available) or self-host the full Supabase stack.

What You Need

  • A Supabase project (free tier works)
  • The Supabase CLI (npx supabase)

Setup Steps

1. Create a Supabase Project

Go to supabase.com/dashboard and create a new project. Note your:
  • Project URL: https://<your-project-ref>.supabase.co
  • Anon Key: Found in Settings > API > Project API keys > anon public
  • Service Role Key: Found in Settings > API > Project API keys > service_role (used by Edge Functions only, never exposed to clients)

2. Apply Database Migrations

From the repo root:
# Link to your Supabase project
npx supabase link --project-ref <your-project-ref>

# Apply migrations
npx supabase db push
This creates three tables with Row Level Security (RLS):
TablePurpose
hostsRegistered machines (name, fingerprint, push token)
commandsIntercepted commands pending approval
grace_periodsActive auto-bypass tokens
All tables have RLS policies ensuring users can only access their own data.

3. Enable Realtime

The migrations automatically add commands and grace_periods to the Supabase Realtime publication. Verify in your dashboard under Database > Replication that both tables are listed.

4. Deploy Edge Functions

The push_notification Edge Function sends push notifications via Expo when commands are inserted:
npx supabase functions deploy push_notification
The function requires these environment variables (set automatically in Supabase):
  • SUPABASE_URL
  • SUPABASE_SERVICE_ROLE_KEY

5. Enable Anonymous Auth

YesPaPa uses Supabase anonymous auth so the daemon can authenticate without user accounts:
  1. Go to Authentication > Providers in your Supabase dashboard
  2. Enable Anonymous Sign-ins

6. Configure YesPaPa to Use Your Supabase Server

During yespapa init, when prompted for the remote server:
Remote server URL [https://remote.yespapa.io]: https://your-project.supabase.co
Remote server key [default]: your-anon-key-here
Or if already initialized, update the config directly:
# The daemon reads these from ~/.yespapa/yespapa.db config table
# You can reinitialize with: yespapa uninstall && yespapa init

Schema Reference

hosts Table

create table public.hosts (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users not null default auth.uid(),
  host_name text not null,
  host_fingerprint text unique not null,
  push_token text,
  last_seen_at timestamptz default now(),
  created_at timestamptz default now() not null
);

commands Table

create table public.commands (
  id text primary key,
  host_id uuid references public.hosts(id) on delete cascade not null,
  command_display text not null,
  justification text,
  status text not null default 'pending'
    check (status in ('pending', 'approved', 'denied')),
  totp_code text,
  denial_message text,
  timeout_seconds integer default 0,
  created_at timestamptz default now() not null,
  resolved_at timestamptz
);

grace_periods Table

create table public.grace_periods (
  id text primary key,
  host_id uuid references public.hosts(id) on delete cascade not null,
  scope text not null,
  expires_at timestamptz not null,
  hmac_signature text not null,
  created_at timestamptz default now() not null
);

RLS Policies

All tables use the same pattern — users can only access rows they own:
-- hosts: direct ownership
create policy "users_own_hosts" on public.hosts
  for all using (auth.uid() = user_id);

-- commands: owned via host
create policy "users_own_commands" on public.commands
  for all using (
    host_id in (select id from public.hosts where user_id = auth.uid())
  );

-- grace_periods: owned via host
create policy "users_own_grace" on public.grace_periods
  for all using (
    host_id in (select id from public.hosts where user_id = auth.uid())
  );

Supabase Troubleshooting

”Remote connection failed” during init

  • Verify the URL is correct and includes https://
  • Check that the anon key is the anon key, not the service_role key
  • Ensure anonymous auth is enabled in your Supabase project

Push notifications not arriving

  • Verify the Edge Function is deployed: npx supabase functions list
  • Check Edge Function logs: npx supabase functions logs push_notification
  • Ensure the mobile app has a valid push token (check hosts.push_token in the database)

Commands stuck as “pending”

  • Check Realtime is enabled for the commands table
  • Verify the daemon is connected: yespapa status should show “Remote: configured”
  • Check daemon logs: cat ~/.yespapa/daemon.log

Security Considerations

These apply regardless of which backend you use.

Zero-Trust Architecture

Even when self-hosting, the remote server is never trusted for security decisions:
  • TOTP codes are validated locally by the daemon, not by the server
  • Grace tokens are HMAC-signed with the TOTP seed, which never leaves the machine
  • The server is a relay — it forwards approval requests and responses, but cannot forge approvals
  • If the server is compromised, the worst case is denial of service (commands stuck pending). Attackers cannot approve commands
  1. Use TLS — Always use HTTPS in production. For the Docker server, place it behind a reverse proxy (nginx, Caddy, Traefik) with TLS termination
  2. Set a strong JWT secret — For the Docker server, use a randomly generated string of at least 32 characters
  3. Restrict network access — Limit who can reach the server port. Use firewall rules or bind to a private network
  4. Set up monitoring — Watch for unusual patterns in the commands table
  5. Back up your database — Especially host registrations and push tokens
  6. Rotate secrets — Periodically rotate JWT secrets (Docker) or API keys (Supabase) and update client configs

Alternative Backends

The Docker server (server/ directory) is the reference self-hosted implementation. It provides the full remote server protocol using Express, WebSocket, SQLite, and JWT authentication. For most self-hosting scenarios, this is all you need. If you need a different stack, the remote server protocol is intentionally simple — a REST API with WebSocket subscriptions. You could reimplement it with:
  • Self-hosted Supabase — Run the full Supabase stack on your own infrastructure using the migrations in supabase/
  • PostgreSQL + PostgREST + pg_notify — Same REST+realtime pattern without Supabase
  • Firebase — Firestore + Cloud Functions + FCM
  • Custom server — Any language/framework that provides REST + WebSocket + push notifications
The daemon needs:
  1. A REST API for CRUD on hosts, commands, grace_periods
  2. Real-time subscriptions for commands and grace_periods changes
  3. A push notification endpoint (Expo Push API compatible)
  4. Token-based authentication (JWT or equivalent)