Trust
What actually happens to your code
Three things you can verify on this page. We never execute your code. We wipe it from our worker in seconds. We never use it to train a model. Everything below is excerpted from real source files in the repo, with paths and line numbers so you can confirm any claim against the file it came from.
The pipeline
Six steps from the moment you hit “scan” to the moment the report renders.
- Ingest. You submit a public repo URL, a zip upload, or a private repo through GitHub OAuth. The web app writes a row to
scansand dispatches the work to our analysis worker. - Clone or extract. The worker clones the repo or unzips the archive into a temporary filesystem on the worker container. Nothing executes. We are reading files, not running them.
- Secrets purge. Before any analysis runs, the worker scrubs
.envand other obvious secret-bearing files from the workdir so they never reach the analyzer or our AI providers. - Static analysis. The analyzer walks files and detects cost drivers. Chunks of source are sent to Anthropic and OpenAI under no-training API terms. The full audit logic stays in our private repo.
- Wipe. A
finallyblock callsshutil.rmtree(workdir)the moment the report is built or the scan fails. The wipe timestamp is stamped on the row. - Persist and render. Only the audit report (verdicts, cost drivers, fix recommendations) is persisted. Postgres row-level security scopes the row to you. The receipt is shown on the report header and downloadable as JSON.
The exact code behind each promise
Read the actual plumbing. Caption above the block says what it proves. File path and line range under the title say where it lives in the repo. If something here ever drifts from the real source, that's a bug in this page. Please email us if you spot one.
The wipe
wtprice-worker/wtprice_worker/server.py·lines 533–548
finally:
cache.close()
# Privacy: remove the cloned repo immediately
try:
shutil.rmtree(workdir, ignore_errors=True)
except Exception:
pass
# Stamp the wipe time AFTER rmtree returns. Inside the finally
# so this fires on failure paths too — the receipt's load-bearing
# claim is "we wiped regardless of how the scan ended."
try:
sb.table("scans").update({
"code_wiped_at": datetime.now(timezone.utc).isoformat(),
}).eq("id", req.scan_id).execute()
except Exception:
passAfter every scan, the worker removes the temporary repo and stamps a wipe timestamp on the row. The block lives inside finally so it runs whether the scan succeeded or failed.
Row-level security
supabase/migrations/007_enable_rls.sql·lines 29–88
-- scans: authenticated readers can only see their own rows.
ALTER TABLE scans ENABLE ROW LEVEL SECURITY;
CREATE POLICY scans_select_own ON scans
FOR SELECT TO authenticated
USING (user_id = auth.uid()::text);
-- scan_status: live progress events scoped to the owning scan.
ALTER TABLE scan_status ENABLE ROW LEVEL SECURITY;
CREATE POLICY scan_status_select_own ON scan_status
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM scans
WHERE scans.id = scan_status.scan_id
AND scans.user_id = auth.uid()::text
)
);
-- Realtime subscriptions: same scope, gated on a signed-in session.
CREATE POLICY scan_status_realtime_own ON scan_status
FOR SELECT TO anon
USING (
auth.uid() IS NOT NULL
AND EXISTS (
SELECT 1 FROM scans
WHERE scans.id = scan_status.scan_id
AND scans.user_id = auth.uid()::text
)
);
-- user_billing: own row only.
ALTER TABLE user_billing ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_billing_select_self ON user_billing
FOR SELECT TO authenticated
USING (user_id = auth.uid()::text);
-- Anon role gets zero read access to scan-uploads. Server reads via
-- service role; the browser only writes uploads, never reads them.
DROP POLICY IF EXISTS "anon can read scan-uploads" ON storage.objects;Postgres enforces per-row ownership on scans, scan_status, and user_billing. The browser's anon key cannot read another user's data, even if every API route were misconfigured. The database is the last line of defense.
Privacy receipt fields
supabase/migrations/016_privacy_traceability.sql·lines 31–46
ALTER TABLE scans
ADD COLUMN IF NOT EXISTS code_received_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS code_extracted_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS code_wiped_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS code_sha256 TEXT,
ADD COLUMN IF NOT EXISTS byte_count BIGINT,
ADD COLUMN IF NOT EXISTS file_count INTEGER,
ADD COLUMN IF NOT EXISTS storage_region TEXT;
COMMENT ON COLUMN scans.code_received_at IS 'When the worker began clone/download. UTC.';
COMMENT ON COLUMN scans.code_extracted_at IS 'When clone/unzip + secrets-purge completed.';
COMMENT ON COLUMN scans.code_wiped_at IS 'When shutil.rmtree(workdir) finished. Set in the worker finally block so it fires on failures too.';
COMMENT ON COLUMN scans.code_sha256 IS 'GitHub: 40-hex HEAD commit SHA. Zip: 64-hex SHA-256 of the downloaded archive bytes.';
COMMENT ON COLUMN scans.byte_count IS 'Bytes of source after secrets-purge + SKIP_DIRS pruning, just before Stage 1 chunking.';
COMMENT ON COLUMN scans.file_count IS 'File count at the same point as byte_count.';
COMMENT ON COLUMN scans.storage_region IS 'Worker region (e.g. us-east-1). For zip uploads the bucket region may be appended.';Every scan row carries the timestamps and SHA-256 that back the receipt: when the code arrived, when it was extracted, when it was wiped, what its hash was, and how many files and bytes were analyzed. You can hash your archive locally and confirm we analyzed exactly what you sent.
GitHub disconnect
wtprice-web/app/api/github/disconnect/route.ts·lines 15–55
export async function POST() {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "not_signed_in" }, { status: 401 });
}
const sb = serverClient();
const { data: conn } = await sb
.from("user_github_connections")
.select("access_token")
.eq("user_id", user.id)
.maybeSingle<{ access_token: string }>();
if (conn?.access_token) {
const clientId = process.env.GITHUB_OAUTH_CLIENT_ID;
const clientSecret = process.env.GITHUB_OAUTH_CLIENT_SECRET;
if (clientId && clientSecret) {
// GitHub's "Delete an app token" endpoint requires Basic auth
// with client_id:client_secret and the access_token in the body.
try {
await fetch(
`https://api.github.com/applications/${clientId}/token`,
{
method: "DELETE",
headers: {
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
Accept: "application/vnd.github+json",
"User-Agent": "preprice-app",
},
body: JSON.stringify({ access_token: conn.access_token }),
},
);
} catch (e) {
console.warn("[gh-disconnect] token revoke failed:", e);
}
}
}
await sb.from("user_github_connections").delete().eq("user_id", user.id);
return NextResponse.json({ ok: true });
}One click in the dashboard hits this endpoint. We attempt to revoke the OAuth token on GitHub's side and then delete our local row. If GitHub is unreachable, the local row is still removed so the access path is closed on our side regardless.
Orphan cleanup cron
wtprice-web/app/api/cron/delete-uploads/route.ts·lines 62–99
// Hourly sweeper: delete any scan-uploads object older than 24 hours.
// The worker normally deletes the zip the moment it's extracted; this
// catches orphans (worker died mid-extract, zip never extracted, etc.).
async function runDeleteUploadsInner(): Promise<NextResponse> {
const sb = serverClient();
const cutoffMs = Date.now() - 24 * 60 * 60 * 1000;
let deleted = 0;
const errors: string[] = [];
const { data: folders, error: listErr } = await sb.storage
.from("scan-uploads")
.list("", { limit: 1000, sortBy: { column: "created_at", order: "asc" } });
if (listErr) {
return NextResponse.json({ error: listErr.message }, { status: 500 });
}
for (const folder of folders ?? []) {
if (!folder.name) continue;
const { data: files } = await sb.storage
.from("scan-uploads")
.list(folder.name, { limit: 100 });
const stale = (files ?? []).filter((f) => {
const ts = f.created_at ? new Date(f.created_at).getTime() : 0;
return ts > 0 && ts < cutoffMs;
});
if (stale.length === 0) continue;
const paths = stale.map((f) => `${folder.name}/${f.name}`);
const { error: rmErr } = await sb.storage
.from("scan-uploads")
.remove(paths);
if (rmErr) {
errors.push(`remove ${folder.name}: ${rmErr.message}`);
continue;
}
deleted += stale.length;
}
// ... stuck-scan recovery + trashed-scan permanent-delete follow.
}Hourly Vercel cron that sweeps the upload bucket and deletes any zip older than 24 hours. The normal path is seconds, inside a worker finally block. This cron exists for the abnormal cases (worker crash mid-extract) so nothing can linger.
Privacy receipt UI
wtprice-web/components/report/privacy-strip.tsx·lines 62–88
// Visible on every report header. Reads code_received_at,
// code_wiped_at, and code_sha256 from the scan row (RLS-protected),
// renders a "Code wiped 0.4s after report" strip, and offers a JSON
// receipt download with the full lifecycle + byte/file counts.
return (
<div className="mb-5 rounded-md border border-outline-variant bg-surface-low px-4 py-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[12px]">
<ShieldCheck className="h-4 w-4 text-ok shrink-0" strokeWidth={2.25} />
<span className="text-ink font-semibold">
{isLegacy ? "Privacy receipt" : wipeLabel}
</span>
<span className="text-ink-muted">·</span>
<span className="text-ink-muted">{sourceNote}</span>
{shortHash ? (
<>
<span className="text-ink-muted">·</span>
<span className="text-ink-muted">
SHA-256 <code className="font-mono text-ink">{shortHash}</code>
</span>
</>
) : null}
<button
type="button"
onClick={onView}
className="ml-auto text-accent-bright hover:text-accent underline underline-offset-2 font-semibold"
>
view receipt →
</button>
</div>
);What you actually see on every report. The strip is render-time read of the RLS-protected scan row. Clicking the link opens a modal that downloads the full receipt as JSON for your compliance trail.
What we send to AI providers
Static analysis identifies the chunks worth sending. Those chunks go to Anthropic and OpenAI through their APIs under no-training and zero data retention contract terms. Both providers operate the API surface under standard 30-day retention. Your code never enters a training set. We do not send your code to any other AI vendor.
The no-training and ZDR settings are configured at the organization level in each provider's console, not as per-request headers. We mention this because some founders ask and we want the answer to be plain.
Other security measures we have taken
- Postgres row-level security on every user-scoped table. The browser's anon key cannot read another user's scans, status, or billing, even if a future API route were misconfigured. The database is the last line of defense.
- Service-role write boundary. Every write path on the server uses the service-role Supabase client. The browser anon role has zero write access to your tables. Realtime progress is read-only and gated by a signed-in session through an EXISTS join on
scans. - Magic-link auth. Single-use tokens, 24-hour expiry, no passwords to leak. Server actions are signed with a stable encryption key pinned across builds so action IDs cannot be replayed from stale bundles.
- HTTPS with HSTS preload. No plaintext path to the app or the API.
- Constant-time compare on cron secrets. The orphan-cleanup endpoint authenticates with
timingSafeEqualso the cron secret cannot be brute-forced via response-time side channels. - CSRF state on OAuth. The GitHub start endpoint issues an HttpOnly state cookie and verifies it on callback. OAuth tokens are stored server-side and never sent to the browser.
- Idempotent Stripe customers. Concurrent checkout requests fold onto the same Stripe customer via an idempotency key so we cannot create duplicate billing accounts under race conditions. Stripe handles all card data directly; we never see card numbers.
- No advertising trackers, no session replay. Product analytics record page views and button clicks. No keystrokes, no form contents, no replays.
What stays private, and why
The audit heuristics, prompts, and cost-model parameters live in our analyzer repo and stay closed. That's the product. The plumbing on this page is everything that touches your data: how we receive it, how we hold it, how we wipe it, who can read it. We're happy to make that part as transparent as possible because it's the only part that matters for trust.
Want to look deeper?
If a code snippet on a marketing page is not enough, send us an email. We will set up a 15-minute live walkthrough where we open the PrePrice codebase on a call and you ask whatever you want. Bring a security checklist, bring a specific worry, bring nothing. Whatever helps.
Frequently asked
What if I do not want to grant OAuth access?
Use a public repo URL or upload a zip. Both flows skip the OAuth handshake entirely. The OAuth path is opt-in for private repos.
What if I want my report deleted?
Open the scan in your dashboard and click delete. The row is moved to trash and permanently removed by the same cron that sweeps orphaned uploads, seven days later. Or email us and we'll do it the same day.
Do you store the code itself?
No. The code lives on a temporary worker filesystem during the scan and is wiped in seconds. Only the audit report (verdicts, cost drivers, fix recommendations) is persisted to our database.
How do I verify the SHA-256 in the receipt matches what I sent?
For a zip upload, run shasum -a 256 your-file.zip locally and compare. For a GitHub repo, the value is the commit SHA at the moment we cloned, so compare against git rev-parse HEAD on the same branch at that timestamp.