Security and data flow
Last updated 14 October 2026
This page is for the people who review a vendor before their organisation uses it: what WattOps runs on your machines, what leaves them, what we keep, and how the service is built. The Privacy Policy and the Data Processing Agreement are the binding documents; this page explains the mechanics behind them. We countersign the DPA and complete security questionnaires on request through the contact form.
In one paragraph
joules, the measurement tool, is open source (MIT), written against the Python standard library only, with no dependencies. It reads the energy counters the hardware already exposes. It sends nothing unless a command asks it to (--track, --share, joules login, or joules agent --track), and then only numbers and short names: never prompts, outputs, command-line arguments, file paths, file contents or code. The website and API run on Vercel; the database and sign-in run on Supabase in the United States. The backend holds no service-role key: it reaches the database only through SQL functions that check a server secret, and every private table also has row-level security as a second lock.
What runs on your side
| Component | What it is | What it reads | Network |
|---|---|---|---|
joules run, joules bench | A command-line tool that wraps one command or benchmarks one local model | GPU energy from NVML (NVIDIA driver library), /sys/class/hwmon (AMD, Intel), CPU and DRAM energy from /sys/class/powercap (RAPL), powermetrics on Apple Silicon, and the command's CPU time | None, unless --track or --share is given |
joules agent | One process per GPU server or cluster node | The meters above; /proc and cgroups to map processes to Slurm jobs, Kubernetes pods, containers and users; GPU memory per process (NVML) and DRM usage per process (AMD, Intel) | Listens on 127.0.0.1:9877 for Prometheus (/metrics, /jobs, /health). With --track, sends each finished job and, on the Max plan, one heartbeat every 5 seconds |
joules proxy | An HTTP proxy in front of a model server | The meters, and token counts from the server's responses | Listens on 127.0.0.1:8787. With --track, one summary per model every 5 minutes |
joules ci | The pull request energy check | The meters, or the command's CPU time on hosted runners | The GitHub API, only with --comment |
Privileges. NVIDIA counters are readable by any user. AMD and Intel GPU counters are readable by any user on most distributions. RAPL counters need root, or a one-time chmod a+r on the energy_uj files (joules doctor prints the exact command). powermetrics on a Mac needs sudo; joules asks once. The agent needs root to read other users' processes, which is what lets it split energy by job. The systemd unit and the Kubernetes DaemonSet we ship (integrations/cluster-agent/) show the exact access each needs.
Updates. Nothing updates itself and nothing phones home. The installers update joules when you run them again; pipx upgrade landauer-gap does the same.
What leaves your machines
The API accepts only the fields listed here, rejects any other field, and caps every request at 32 KB. --json prints the full local report, of which the sent record is a subset.
| Command | What is sent | When |
|---|---|---|
joules run --track, joules bench --track | Project name; a label that is the program and script name (python train.py) or your --label; start time; duration; energy; cost; CO₂; electricity price; grid intensity; the GPU model names or CPU name; GPU count; whether idle was subtracted; whether meters were simulated; joules version; which meter kinds were used; operations and Landauer gap if you gave --params or --tokens; exit code; for a bench, output tokens and joules per token; on NVIDIA GPUs, what the card was doing as percentages (clock as a share of maximum, busy, memory-controller busy, share of time throttled), its hottest temperature, its power limit, the main throttle reason and a one-word reading (compute, memory, idle, mixed) | Once, when the run ends |
joules bench --share | Model name, backend, device, device count, energy per token, tokens per second, average power, joules version | Once, when you ask. The public leaderboard shows medians only, never who shared |
joules agent --track | Each finished job as a tracked run (the fields above, with the job name as the label and the Unix user as the user) | When a job finishes |
joules agent --track on the Max plan | A heartbeat: node name (--name sets it), watts, idle watts, GPU count, idle GPU count, price, grid intensity, and up to 50 jobs with name, user, kind, GPU count, watts and idle watts | Every 5 seconds; each heartbeat is deleted after 6 hours |
joules proxy --track | One tracked run per model with the model name as the label | Every 5 minutes |
joules login, joules doctor | login sends the key once to check it. doctor checks the key and the connection unless --offline is given, and its report contains no key, machine name, user name or home folder | When you run them |
Never sent: prompts, model outputs, command-line arguments or their values, inline code, environment variables, file paths, file contents, or your machine name (the agent's node name is the one exception, and --name controls it).
How the service is built
- Hosting. The site and API are serverless functions on Vercel. The database and sign-in are Supabase Postgres and Supabase Auth in the United States (us-east-1). Payments are Stripe.
- Database access. The backend has no service-role key. It can only call the
lg_*SQL functions, each of which first checks a server secret; only the secret's SHA-256 is stored in the database. Private tables live in a schema the Supabase API does not expose, and every private table has row-level security switched on as a second lock. - Sign-in. Supabase Auth: email and password, an emailed sign-in link, GitHub or Google. Passwords are hashed by Supabase Auth and never reach our code. Sessions live in your browser's local storage and refresh themselves.
- API keys are random 238-bit secrets shown once; only their SHA-256 is stored, and revocation is immediate. A team key acts only for its team, and only while the person who made it is still a member.
- Metering is one atomic SQL call per request: the monthly quota and the per-minute burst are checked and counted together.
- Input validation. Every endpoint accepts known fields only, checks numeric ranges, and caps request bodies at 32 KB. Errors are JSON with a code, a message and the fields concerned.
- Browser. A strict Content Security Policy with no inline scripts, HTTP Strict Transport Security with preload,
frame-ancestors 'none',X-Content-Type-Options: nosniffand a strict referrer policy. The one third-party script (the sign-in library) is pinned with Subresource Integrity. There are no analytics or tracking scripts and no third-party cookies. - Billing. Stripe Checkout and the Stripe customer portal. Webhooks are signature-checked on the raw body, and the subscription is re-read from Stripe so events arriving out of order cannot leave a wrong plan. Card numbers never reach us.
- Encryption. TLS on every connection to the site, the API and the database. Data at rest is encrypted by Supabase.
- Secrets you give us. An alert's webhook address is returned only to the person who set it. The hashed IP address on the contact form is used only to limit abuse.
- Logs. Vercel keeps standard request logs (IP address, browser) for a short time. We keep no analytics.
Retention and deletion
- Account data is kept until you delete your account. Deleting it (Settings → Delete account) cancels a paid subscription first, then removes saved runs, tracked runs, keys, usage, measurements, alerts and any team you own.
- Live heartbeats are deleted automatically after 6 hours.
- Usage counters older than the current month are kept only as monthly totals. Revoked API keys are kept as records, without the key, so old usage still adds up.
- Billing records are kept as long as tax law requires (in Canada, generally six years).
- Settings → Your data gives you everything we hold about you as one JSON file.
Sub-processors
| Service | What it does for us | Where |
|---|---|---|
| Supabase, Inc. | Database and sign-in | United States (us-east-1) |
| Vercel Inc. | Website and API hosting | United States and Vercel's global network |
| Stripe, Inc. and its affiliates | Payments (billing contacts only) | United States and other countries |
| jsDelivr | Serves the sign-in library to your browser | Global network |
| Google Fonts | Serves the typefaces to your browser | Global network |
| GitHub and Google | Your profile picture, if you signed in with them | United States |
We update this list at least 14 days before a new sub-processor starts processing personal data, as the DPA sets out.
Assurance
- Every change runs the full test suite before it is merged: the API tests against a real Postgres with every migration applied, browser tests of every page and button, and the joules unit tests on Linux, macOS and Windows. Every week the tests also install joules on fresh machines the way a new user would.
- The database's own security and performance advisor has been run and its findings applied (indexes on every foreign key, row-level security on every private table).
- The code has had a security review and its fixes are in place. WattOps does not yet hold a SOC 2 or ISO 27001 certification and has not yet commissioned an independent penetration test; we answer security questionnaires and support audits under the DPA.
Reporting a vulnerability
Use the contact form and say it is a security report; we reply by email. Our security.txt is at wattops.dev/.well-known/security.txt. Please test only against accounts you own.
If something goes wrong
Under the DPA we tell you without undue delay after we become aware of a personal data breach affecting your data, with the information you need to meet your own obligations. Where a breach creates a real risk of significant harm, we also report it to the Privacy Commissioner of Canada as the law requires.
PHENEX TECHNOLOGIES INC., Vancouver, British Columbia, Canada.