Adds a pink/purple/blue SVG brand mark (favicon + header) and reworks the visual language around the app's actual interaction - fader-driven device control - via LED-style indicators, mono instrument readouts, hairline-framed cards, console-tab nav, and an oscilloscope-style session timeline. Leaves components/ui/* untouched, restyling only via design tokens and the app's own composed components. Also fixes a proxy.ts matcher gap that was gating /icon.svg behind auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb
148 lines
8.2 KiB
Markdown
148 lines
8.2 KiB
Markdown
# SEXY
|
|
|
|
A self-hosted web console for scanning, connecting to, and controlling Bluetooth LE sex toys over the
|
|
[Buttplug](https://buttplug.io) protocol - the same open-source protocol behind Intiface Central and most
|
|
other legitimate Bluetooth toy-control apps. Record a live control session, save it as a named recording,
|
|
and replay it later against a (possibly different) set of connected devices. Track usage in a stats
|
|
dashboard. All gated behind a single shared secret, all data kept in a local SQLite file.
|
|
|
|
## Architecture, in words
|
|
|
|
Web Bluetooth (`navigator.bluetooth`) only exists inside a browser, in a secure context (HTTPS or
|
|
`localhost`), and requires a user gesture to start a device scan. A Docker container has no access to host
|
|
Bluetooth hardware at all. So:
|
|
|
|
- **The actual Buttplug client and device connector run entirely in your browser**, using `buttplug-wasm`
|
|
as an *embedded* connector - a full Buttplug server compiled to WebAssembly, talking to devices directly
|
|
over Web Bluetooth. There's no separate Intiface Engine or Buttplug Server process to run.
|
|
- **The Next.js server is never in the real-time device-control loop.** It only handles the login gate,
|
|
persists recordings and session telemetry to SQLite, serves the stats aggregation endpoints, and serves
|
|
recordings back for replay - all over plain HTTPS/JSON, batched every few seconds, not per slider tick.
|
|
|
|
```
|
|
Browser Server (Next.js)
|
|
┌─────────────────────────┐ ┌───────────────────────┐
|
|
│ buttplug-wasm (embedded)│ │ auth / API routes │
|
|
│ ↕ Web Bluetooth │ HTTPS │ recordings + sessions │
|
|
│ BLE devices │ ───────► │ stats aggregation │
|
|
│ live control / replay │ (batch) │ SQLite (better-sqlite3)│
|
|
└─────────────────────────┘ └───────────────────────┘
|
|
```
|
|
|
|
## Browser support (read this first)
|
|
|
|
- **Works:** Chrome or Edge on desktop, Chrome on Android, over HTTPS (or `localhost` in development).
|
|
- **Does not work, at all, on any browser: iOS and macOS Safari.** Every browser on iOS is WebKit under
|
|
the hood by Apple's policy, and WebKit has no Web Bluetooth implementation. This is a platform limitation,
|
|
not a bug in this app - there's no workaround short of Apple shipping Web Bluetooth support.
|
|
- The app itself detects `'bluetooth' in navigator` and shows a clear message instead of a broken UI on
|
|
unsupported browsers.
|
|
|
|
## Features
|
|
|
|
- Scan for and connect to multiple Bluetooth LE devices at once
|
|
- Live per-actuator control (vibrate / rotate / linear) with a responsive slider UI
|
|
- Start/end sessions; save a completed session as a named, replayable recording
|
|
- Replay a recording against a different set of connected devices, with a device-remap step (BLE exposes no
|
|
stable device id across sessions, so recorded devices are matched to live ones by name, with manual
|
|
override when needed)
|
|
- Stats dashboard: session summaries, per-session intensity timelines, per-device usage, recording-library
|
|
stats (play counts, average length)
|
|
- Single shared-secret login, no user accounts
|
|
- SQLite storage, Docker Compose deployment, Gitea Actions CI
|
|
|
|
## Tech stack
|
|
|
|
Next.js (App Router) · Tailwind CSS v4 · shadcn/ui · `buttplug` + `buttplug-wasm` · Drizzle ORM +
|
|
better-sqlite3 · zustand · jose · pnpm
|
|
|
|
## Local development
|
|
|
|
Requires Node 22+, pnpm, and a Chromium-based browser (Chrome or Edge) for actually exercising the Bluetooth
|
|
parts - `http://localhost:3000` counts as a secure context, so no HTTPS setup is needed in dev.
|
|
|
|
```bash
|
|
pnpm install
|
|
cp .env.example .env # fill in ACCESS_PASSWORD and AUTH_SECRET
|
|
pnpm db:generate # only needed after changing lib/db/schema.ts
|
|
pnpm dev
|
|
```
|
|
|
|
Migrations run automatically against `DATABASE_PATH` on server boot (see `instrumentation.ts`) - no manual
|
|
migrate step needed for a normal `pnpm dev` / `pnpm start`. `pnpm db:migrate` exists for applying migrations
|
|
out-of-band if you ever need to.
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Required | Description |
|
|
| ------------------ | -------- | ----------------------------------------------------------------------------- |
|
|
| `ACCESS_PASSWORD` | yes | The shared secret typed into the login form. Compared in constant time. |
|
|
| `AUTH_SECRET` | yes | Signing key for the session JWT. Must differ from `ACCESS_PASSWORD` - keep both secret; a leaked signing key lets an attacker forge sessions. |
|
|
| `DATABASE_PATH` | no | Path to the SQLite file. Default `./data/app.db`. |
|
|
| `LOG_LEVEL` | no | `debug` \| `info` \| `warn` \| `error`. Defaults to `debug` in dev, `info` in production. |
|
|
|
|
Compose-only (read by `docker-compose.yml` for `${...}` substitution, not by the app itself):
|
|
`TRAEFIK_HOST`, `NETWORK_NAME`.
|
|
|
|
## Database
|
|
|
|
Schema lives in `lib/db/schema.ts`, migrations in `drizzle/` (generate new ones with `pnpm db:generate`
|
|
after a schema change, and commit the generated SQL). A recording is a thin pointer over an already-captured
|
|
session's events, not a separate capture pipeline - every live command is always persisted for stats,
|
|
regardless of whether the session is later saved as a named recording.
|
|
|
|
## Docker deployment
|
|
|
|
```bash
|
|
mkdir -p data && chown 10001:10001 data # match the container's non-root uid/gid
|
|
cp .env.example .env # fill in real secrets + TRAEFIK_HOST/NETWORK_NAME
|
|
docker compose up -d --build
|
|
```
|
|
|
|
**HTTPS in front of this container is not optional.** Web Bluetooth refuses to run outside a secure
|
|
context, so if the app isn't served over HTTPS (via Traefik, as the included labels assume, or another
|
|
reverse proxy terminating TLS), the Control and Replay pages simply won't be able to use Bluetooth at all.
|
|
|
|
The SQLite file lives at `./data/app.db` on the host (bind-mounted, read-write) - everything else in the
|
|
container filesystem is read-only. Back up by copying that one file; there's no other state.
|
|
|
|
## CI/CD
|
|
|
|
`.gitea/workflows/ci.yml` runs lint + build on every push/PR, and on a pushed tag also builds and pushes the
|
|
Docker image to this repo's path on the `dev.pivoine.art` Gitea registry, authenticating with a
|
|
`PACKAGE_TOKEN` repo secret.
|
|
|
|
## Usage guide
|
|
|
|
1. Log in with the shared secret.
|
|
2. On **Control**, scan for devices and connect. Move the sliders to control actuators live.
|
|
3. Click **Start session** before you begin if you want this session tracked in stats or saved as a
|
|
recording; **End session** when done, and optionally name it to save it as a recording.
|
|
4. On **Recordings**, click replay on a saved recording, connect the devices you want to use, match them to
|
|
the recording's original device slots, and play back.
|
|
5. **Sessions** and **Stats** show history and aggregated usage.
|
|
|
|
## Security & privacy notes
|
|
|
|
- This is a single shared-secret gate, not multi-user authentication - anyone with the secret has full
|
|
access. Treat it like a shared house key; restricting network access (VPN, IP allowlist, Traefik
|
|
middleware) in addition to the app secret is recommended for anything beyond trusted personal use.
|
|
- All data (recordings, session history, device names) stays in your own SQLite file. Nothing is sent
|
|
anywhere except directly between your browser and the devices it connects to, and between your browser
|
|
and this server.
|
|
- There's no login rate-limiting in this version - acceptable behind a private deployment, but worth
|
|
knowing.
|
|
|
|
## Known limitations
|
|
|
|
- **iOS/Safari cannot use Web Bluetooth at all** (see above) - not fixable from this app.
|
|
- BLE device identity isn't stable across browser sessions; the device-remap step mitigates but can't fully
|
|
solve matching two identically-named devices.
|
|
- Concurrent BLE connection limits are set by the OS/Bluetooth adapter, not this app.
|
|
- Replay timing uses `performance.now()`-relative scheduling to avoid wall-clock drift, but individual
|
|
command dispatch still has a few-to-tens-of-milliseconds of jitter.
|
|
|
|
## License
|
|
|
|
MIT, see [LICENSE](./LICENSE).
|