┌──────────────────────────────────────────────────────────────────────────────────┐
│  T55 · WEBSITE REDESIGN + REPOSITIONING                                          │
│  Status: DRAFT · Owner: Lazy Coder · Target ship: V1 by 2026-06-15               │
└──────────────────────────────────────────────────────────────────────────────────┘
T55 · v1 · 2026-05-15

Reposition the product. Gate the door. Ship the panel.

A full rebuild of the public site and the auth/billing surface around it. Reframes browser-recon from "scraper tool" to "AI agent for scraping reconnaissance." Adds a waitlist gate, tier-based credits, and an admin panel that grants access and curates the marketing surface.

Status
DRAFT
Version
v1
Identity
Terminal · ultraviolet
Stripe
deferred → v2
Target
2026-06-15
verdict

Ship a single redesign that does four things at once: re-frames the product as an AI agent (not a scraper), gates new signups behind a waitlist with an optional tweet jump-line, replaces magic-link auto-create with ACTIVE-only login, and introduces four paid tiers with monthly-expiring credits and tier-gated report retention.

V1 is admin-granted credits — no card processing during beta. Stripe checkout is wired in V2 once the first cohort has shaken out the pricing surface. The admin panel ships with waitlist queue management, tweet template CRUD, and a live theme accent switcher stored in app_config.

01 — rationale

Three things are wrong with the current surface.

The product has matured faster than the public framing around it. The site sells a 2025 scraper-tool; the product behaves like a 2026 agent. Closing that gap unlocks pricing power, audience clarity, and a clean place to put the waitlist.

// problem 01

Anybody can sign up

Magic-link auto-creates a user. No vetting, no demand-shaping, no way to batch onboarding. We get drive-by users who never come back, and no story for "scarcity."

// problem 02

The product is mis-framed

The current copy reads as a scraping toolkit. The actual value is an autonomous reconnaissance pass over a target site. Same product, but the words don't match what users get.

// problem 03

No paid path

No tiers, no credit packs, no retention model. Every report is kept forever or thrown away — there's no shape to monetise against. Free users cost us blob storage forever.

The redesign fixes all three with a single shipped artefact: a new landing + pricing + user-guide trio, a waitlist gate in front of the magic-link flow, four monthly tiers with tier-gated retention, and an admin panel to operate the queue, the templates, and the theme.

02 — positioning

What we say. What we don't.

Every line on the public surface is a deliberate choice between marketing pull and operational risk. The table below is the source of truth — copywriters work from this, not from earlier drafts.

SayDon't say
AI agent for scraping reconnaissance. Scraper. We're not — our users build them. We're the recon pass before they do.
Measurement-first. Real test requests through real proxies. "Powered by Claude / OpenAI / Grok." We may swap providers any week; locking into a brand badge is a future-tax.
Tested against Walmart, Staples, Target, Airbnb, Ticketmaster, CoinMarketCap. Vague volume claims ("thousands of sites scanned"). We have six real ones — use the names.
TLS 1.3 in transit. AWS KMS (AES-256) at rest. Sensitive values scrubbed before storage. "End-to-end encryption." Materially false — the server has plaintext access during validation. Saying this is legally regulated marketing language.
Runnable starter code. Drop into your stack. Pipeline diagrams. The orchestrator / sweeper / synthesis flow is our IP. Don't draw it.
Invite-only beta · N spots remaining. Fake user counts ("Trusted by 10,000+ developers"). Bad risk-reward; trust-precedent loss is permanent.
A two-minute browse is enough. "Zero-config" / "automatic" — overstates. The user still picks an intent template.
What the agent does: detect anti-bot, validate library × proxy, project cost, generate starter code. How the agent does it. The chain of LLM calls, the prompts, the scoring rubric — internal only.

One bold line for the hero, one for the deck, the rest of the page does the work. The hero does not claim numbers we can't back up. The "tested against" strip is the credibility load-bearer in V1 — it's all real, all named, all from the existing browser-recon verification set.

03 — site map

Seven public routes. Three gated. One admin.

Everything below /admin is staff-only (existing auth pattern). /dashboard and /r/<slug>/edit require waitlist_status = ACTIVE. Everything else is open.

browser-recon.com / landing /pricing public /user-guide public /waitlist post-signup /login magic-link /r/<slug> public report [ ACTIVE ] /dashboard active only /account active only /admin/* staff only

Route notes

/
Landing — hero, tested-against, process, features, dashboard mock, report mock, pricing, waitlist CTA.
/pricing
Dedicated pricing page — same tiers as the landing block but with FAQ + retention timeline + comparison table.
/user-guide
Single long page — install, first scan, what's in a report, what we don't save, security model, FAQ.
/waitlist
Two purposes: post-signup confirmation + the "still on the list" page shown to WAITLIST users who try to log in. Hosts the per-user tweet-submission form.
/login
Magic-link request form. POST checks status and either sends the link, shows the still-waitlisted page, or creates a new WAITLIST row.
/r/<slug>
Public report (existing).
/dashboard
ACTIVE users only — credit balance, recent scans, tier, expiry dates.
/admin/*
Staff-only — waitlist queue, user CRUD, templates, config, KPIs.
04 — waitlist

Two states. One queue. One optional shortcut.

The waitlist is the entire gate. Status is binary: WAITLIST or ACTIVE. Tweet submission is a priority hint to the admin queue, not a separate state. Admins decide who gets in; the tweet just changes the order.

anon signup(email) WAITLIST awaiting access + tweet_url (priority hint) admin_grant() + welcome email · +2 free scans ACTIVE may log in & scan (login, scan, renew) existing users at migration time: ACTIVE (grandfathered)

Signup paths

  • Waitlist form on the landing page or /pricing — email only.
  • Login attempt with an unknown email — silently creates a WAITLIST row and shows the "you're in line" page.
  • Tweet submission page (linked from the welcome-to-waitlist email + visible on /waitlist) — captures a tweet URL on the existing WAITLIST row.

Tweet jump-line — operational

The tweet is optional. Forced advocacy gets fewer + worse tweets than voluntary. The admin queue surfaces the tweet URL next to each entry — admin verifies the post manually and approves the user. The DB shape is intentionally flat: one nullable tweet_url column on the user row, no separate "verification" table.

What makes the admin's job fast: the queue page shows the tweet rendered inline (via X's embed/oEmbed) and a [approve] button that flips the status and triggers the welcome email. Single click. No mental tax.

05 — auth flip

Magic link fires for ACTIVE only.

Today, any email gets a magic link and an auto-created user. After this change, the magic link is gated on waitlist_status = ACTIVE. Unknown emails get added to the waitlist silently — the user sees the same "you're in line" page either way.

POST /auth/magic-link { email } user exists? lookup by email no create WAITLIST row silent · status=WAITLIST yes status? WAITLIST ACTIVE send magic link "check your email" show /waitlist page tweet form · NO link sent

Existing users

Every user that exists at migration time is set to waitlist_status = ACTIVE. Nobody currently using the product loses access. The migration is one UPDATE statement (see section 11).

Edge cases

CaseBehaviour
WAITLIST user clicks an old magic linkToken validation succeeds but session creation rejects because status != ACTIVE redirect to /waitlist.
ACTIVE user removed by adminStatus flipped back to WAITLIST. Existing sessions invalidated. User shown the still-on-list page on next request.
Same email submitted twice on /waitlistUpsert. Tweet URL updates if newly supplied; created_at doesn't change.
User on /login with unknown emailSilent WAITLIST row + same "in line" page as known WAITLIST. No "user not found" leak.
06 — pricing tiers

Four tiers. One trial. One enterprise call.

Tester is a once-per-lifetime first-month trial — enforced server-side, not on the honour system. Beginner, Pro, and Pro Max are the recurring tiers. Enterprise is a "talk to us" call with a real human; no self-serve quote tool in V1.

Tier Price Credits Report retention Notes
Tester $5 / mo 5 2 weeks FIRST MONTH ONLY   lifetime cap = 1 purchase
Beginner $10 / mo 12 1 month Recurring monthly
Pro $20 / mo 30 2 months Recurring monthly · FEATURED
Pro Max $60 / mo 100 3 months Recurring monthly
Enterprise contact negotiated negotiated Custom report shape · framework-specific starter code · SLA

Pricing rules

Tester gate
User can buy Tester only if users.tester_used_at IS NULL. After purchase, that timestamp is set and never cleared.
2 free scans
Granted on transition WAITLIST → ACTIVE, independent of tier purchase. Land before any paid plan.
Tier change
V1: admin sets users.tier. V2: self-serve. No proration in V1 — admins choose effective date.
Trial → paid
A Tester user upgrading to any paid tier immediately ends the trial; remaining trial credits expire on tier change.
07 — credits + retention

Credits expire. Reports do too — at different speeds.

Credits reset every billing month. No rollover, no banking. Reports stay live for the duration of the user's tier — long enough that re-scanning is rare, short enough that we don't carry blob storage forever.

Credit lifecycle

  • Granted at start of billing month (or at activation, for the 2 free scans).
  • Decremented by 1 per scan started. Re-scan from an expired report = same 1 credit.
  • Expired at end of billing month. The next month's grant overwrites; it does not accumulate.

Report retention by tier

Tester
2 weeks
Beginner
1 month
Pro
2 months
Pro Max
3 months

What "expired" means

Past scans.expires_at, the report page renders a stripped-down placeholder: scan metadata visible (domain, date, intent), report body hidden, and a [Scan again — 1 credit] CTA. Clicking starts a fresh scan from scratch — not a re-render of cached analysis. Costs 1 credit.

The DOWNLOAD report button is commented out in V1. Reasons: keeps users coming back to our domain (analytics + retention), avoids the IP-leak of shipping a portable report file, simplifies the V1 surface. Re-enable later if a paying user asks.

Expiry computation

At scan-complete time: scans.expires_at = scans.completed_at + tier_retention(scans.user_id). The retention is read from the user's tier at completion time and frozen onto the row. If the user later downgrades, existing reports keep their original expiry; only new scans pick up the shorter retention.

08 — v1 billing

Admin grants. Stripe waits.

V1 ships without self-serve checkout. Payments are handled out-of-band; admins flip a tier on the user record and credits land. The Stripe webhook code in the repo stays dormant — no checkout sessions, no price catalog, no plan upgrade flow on the customer side.

Why deferred

// reason 01

Tighter feedback loop

First-cohort pricing will move. Iterating in Stripe (price IDs, upgrade rules, proration) costs an order of magnitude more than iterating in our own admin panel.

// reason 02

Less surface to secure

No PAN data, no PCI footprint, no card-disputes risk in beta. Stripe Checkout is hosted and low-risk, but it still adds a webhook attack surface we don't need until we have buyers.

// reason 03

Manual = personal

Each early-buyer conversation is intel. A Stripe form is silent; an email exchange tells us who they are, what they're scraping, why our pricing felt right or wrong.

V1 collection

Pricing page CTA reads "Join the waitlist" — not "Buy now." A user who reaches out for a paid tier emails us (Enterprise CTA already points to mailto). Admin verifies payment via whichever channel makes sense (invoice, bank transfer, Stripe Payment Link generated ad-hoc), then sets users.tier and grants credits.

V2 outline (next phase)

What V2 adds on top of V1
1. Stripe Checkout sessions, one per tier (price IDs in app_config).
2. Webhook receiver wired to the existing handler stub in browser_recon_server/stripe/.
3. /account/billing page — current tier, next renewal, change tier, cancel.
4. Tester-gate enforcement at checkout (Stripe metadata + server check).
5. Proration: downgrade at end of period; upgrade immediate w/ proration credit.
6. Invoice emails (Stripe-hosted) + receipt PDFs.
7. Dunning: 3 retry attempts, then auto-downgrade to read-only.
09 — user guide

One long page. Nine sections.

The user guide is the single source of truth for "how do I use this thing" and "what does this thing do with my data." Everything a paying user needs to read at least once lives here.

§SectionWhat's in it
1InstallOne pipx command. Supported Python (3.11+). What gets installed locally vs server-side.
2First scanWalk through recon scan <url>: intent template, browsing, Ctrl+C, what happens next.
3What's in your reportVerdict, library recommendation, headers, cookies, cost projection, starter code, replay results.
4What we don't saveExplicit scrubbed-fields list. Why we scrub. What "scrubbed" means technically.
5Security modelTLS 1.3 in transit + AWS KMS (AES-256) at rest. Honest framing — not end-to-end.
6Pricing & creditsTiers, monthly expiry, retention, 2 free scans, re-scan cost, Tester one-time rule.
7DashboardRecent scans, credit balance, tier renewal date, expiry dates per report.
8FAQ~10 questions: data retention, credits expiry, proxy support, blocked sites, custom intents.
9TroubleshootingChrome won't launch, scan stuck, report says replay failed, credit not deducted.

"What we don't save" — full list

Section §4 is the load-bearing section of the user guide for trust. It must be specific. Vague claims ("we strip sensitive data") read as marketing; named fields read as engineering.

Field typeTreatmentWhy
Cookie valuesStripped before long-term storage; cookie names kept.Names are non-secret and useful for scraping recon. Values authenticate.
Authorization headerRemoved entirely.Always a bearer token, always sensitive.
X-Api-KeyRemoved entirely.API keys are credentials.
X-Auth-TokenRemoved entirely.Convention varies, but the name is reserved for auth.
X-CSRF-TokenRemoved entirely.Pairs with a session — both removed.
JWT-shaped strings in bodiesReplaced with <redacted-jwt>.Regex matches eyJ… three-segment base64.
Bearer-token shaped stringsReplaced with <redacted-token>.Heuristic match on long opaque alphanumeric blobs in known auth-y contexts.
Session cookies (specific names)Removed entirely (names + values).Named matches against the common list: sessionid, JSESSIONID, PHPSESSID, connect.sid, etc.

Security framing — what to write

# /user-guide §5

Encryption in transit:  All traffic between your machine and our servers
                        uses TLS 1.3.

Encryption at rest:    Captures are stored in AWS S3, encrypted with
                        AWS KMS (AES-256).

What's scrubbed:       Cookie values, Authorization headers, X-Api-Key,
                        X-Auth-Token, X-CSRF-Token, JWT-shaped strings,
                        and named session cookies are stripped before
                        long-term storage. (Full list above.)

What we do see:        We need plaintext access during validation
                        — that's how the agent confirms a library/proxy
                        combination actually works against the target.
                        We do NOT claim end-to-end encryption.

Who can read it:       browser-recon staff, when a support case
                        requires it, with audit logging.

Deletion:              Captures auto-delete at report expiry. The
                        report itself is also deleted at expiry.
10 — admin panel

Six routes. One operator.

The admin panel is for one human (us, at first). Optimise for keystroke economy, not enterprise RBAC. Everything below sits under /admin/*, gated on users.is_staff = true.

RouteWhat it doesHot actions
/admin/waitlist Queue of WAITLIST users, newest first. Inline tweet preview when tweet_url present. approve defer reject
/admin/users Filterable user list. Per-user: edit tier, adjust credits, force-expire, deactivate. grant credits change tier
/admin/scans Recent scan activity across all users. Drill into orchestrator logs, view raw blob. view logs retry
/admin/templates CRUD on tweet templates. Each row: text body, position, active flag, last-used. add edit archive
/admin/config Singleton config: theme accent (hex + preset name), X handle, waitlist depth display, feature flags. save
/admin/metrics KPI cards: waitlist depth, approval rate, MAU, scans/week, success rate, expired-storage in GB. read-only

Theme switcher — operational shape

The theme picker shipped in the design D mockup becomes the canonical admin control. Same swatches, same custom hex input. On save, writes theme_accent_hex + theme_accent_name to app_config. The base Jinja template reads these at request time (with a 60s in-memory cache, invalidated on save) and injects --accent as a CSS custom property in :root.

All customer-facing pages (landing, pricing, user guide, waitlist, dashboard, report, login) pick up the change on next page load. The admin panel itself uses a fixed neutral accent so the operator can preview without committing.

Theme injection — code shape
# browser_recon_server/web/templates/base.html (head)
<style>
  :root {
    --accent: {{ app_config.theme_accent_hex | default("#c77dff") }};
  }
</style>

# browser_recon_server/web/deps.py
def get_app_config(): # 60s LRU cache
    return AppConfig.load_singleton()

# browser_recon_server/admin/routes.py
@router.post("/admin/config")
def save_config(...):
    AppConfig.update(**form_data)
    get_app_config.cache_clear()  # bust the cache
11 — database schema

Two new tables. Five new columns. One migration.

The shape is intentionally flat. Status as a column on users (not a separate table). Tweet URL as a nullable column (not a join table). App config as a key/value singleton (not a typed config row). Less to maintain, faster to query.

-- alembic revision T55_v1_redesign --

ALTER TABLE users
  ADD COLUMN waitlist_status   VARCHAR(16)    NOT NULL DEFAULT 'WAITLIST',
  ADD COLUMN activated_at      TIMESTAMPTZ    NULL,
  ADD COLUMN tier              VARCHAR(16)    NULL,         -- tester|beginner|pro|pro_max|enterprise
  ADD COLUMN tester_used_at    TIMESTAMPTZ    NULL,
  ADD COLUMN tweet_url         TEXT           NULL,
  ADD COLUMN x_handle          VARCHAR(32)    NULL,
  ADD COLUMN credits           INTEGER        NOT NULL DEFAULT 0,
  ADD COLUMN credits_renew_at  TIMESTAMPTZ    NULL;

CREATE INDEX ix_users_waitlist_status ON users (waitlist_status);

-- grandfather: every existing user is ACTIVE; new signups default to WAITLIST
UPDATE users SET waitlist_status = 'ACTIVE', activated_at = NOW() WHERE id IS NOT NULL;

ALTER TABLE scans
  ADD COLUMN expires_at        TIMESTAMPTZ    NULL;

CREATE INDEX ix_scans_expires_at ON scans (expires_at) WHERE expires_at IS NOT NULL;

CREATE TABLE tweet_templates (
  id           UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  body         TEXT         NOT NULL,
  position     INTEGER      NOT NULL DEFAULT 0,
  active       BOOLEAN      NOT NULL DEFAULT TRUE,
  created_at   TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  updated_at   TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE TABLE app_config (
  key          VARCHAR(64)  PRIMARY KEY,
  value        TEXT         NOT NULL,
  updated_at   TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  updated_by   UUID         REFERENCES users(id)
);

-- seed defaults
INSERT INTO app_config (key, value) VALUES
  ('theme_accent_hex',  '#c77dff'),
  ('theme_accent_name', 'ultraviolet'),
  ('x_handle',          'browser_recon'),
  ('waitlist_visible_depth', '247');

Field-level decisions worth keeping

waitlist_status
VARCHAR not ENUM — easier to add states later without a type migration.
tier
Nullable. NULL means "no paid plan" — the 2 free scans on activation don't require a tier.
tester_used_at
Lives on users, not on subscriptions/orders. One-time-only is a user-level fact.
tweet_url
Plain text. No validation in DB — application layer rejects non-https/non-x.com URLs.
scans.expires_at
Frozen at scan-complete time. Downgrades don't retroactively expire old reports.
app_config
Singleton-row pattern via PK = key. Two-column simplicity beats a typed config row.
12 — email copy

Three emails. Plain text. No HTML.

Transactional emails on the waitlist surface are short, plain, and signed by a human. No CSS, no logo, no preview-text engineering. The signal "this came from a real product" lands harder when the email looks like one a developer would send.

Email 01 — Joined the waitlist

Email 02 — Approved (admin grants access)

Email 03 — Tweet received (auto, before approval)

13 — build sequence

Ten PRs. In this order.

Each step is sized to ship in one PR. Skip nothing; the ordering matters because the auth flip in step 4 has to land after the schema migration in step 1, and the admin panel depends on the schema being live.

1

Schema migration

Single alembic revision: users columns, scans.expires_at, tweet_templates, app_config, grandfather UPDATE. Seed default config.

~ 0.5d
2

Theme injection in base template

Read app_config, inject --accent at request time, 60s LRU cache. Verify on existing report pages — purple accent everywhere.

~ 0.5d
3

Public landing + pricing + user guide

Three Jinja templates wired into FastAPI. Identity from design D. Tweet templates pulled from tweet_templates; X handle from app_config. No JS frameworks.

~ 2d
4

Auth flip + /waitlist + /login

POST /auth/magic-link branches on status. Unknown email → silent WAITLIST + show /waitlist page. Existing users grandfathered in step 1. Tweet URL submission on /waitlist.

~ 1.5d
5

Transactional emails

Three plain-text templates (join, approve, tweet ack). Triggered from waitlist endpoint + admin approve action. Existing SES integration or equivalent.

~ 1d
6

Admin panel — waitlist queue

/admin/waitlist with embedded tweet preview, [approve] button. Flips status, grants 2 free scans, fires approval email. Admin auth via existing is_staff.

~ 1d
7

Admin panel — config + templates

/admin/config (theme + handle), /admin/templates (CRUD). Cache invalidation on save. Preset swatches mirror the design D picker.

~ 1d
8

Credits + retention enforcement

Credit-decrement on scan start. scans.expires_at set at completion. Report view checks expiry and renders the [Scan again] CTA past it. Comment out the DOWNLOAD button.

~ 1.5d
9

Dashboard

/dashboard for ACTIVE users — recent scans, credit balance + renewal, tier, retention per report. Reuse the design D mock as the template.

~ 1d
10

Admin panel — users + scans + metrics

/admin/users (filter, edit tier/credits), /admin/scans (recent activity drill-in), /admin/metrics (KPI cards). Read-only metrics in V1.

~ 1.5d

Sequencing rule: step 1 lands first because everything reads from new columns. Step 4 (auth flip) must land before step 6 (admin approve) so the [approve] button has something to flip. Step 8 (retention) and step 9 (dashboard) can swap order if useful. Estimated total: ~11.5 days for one engineer, single-threaded.

14 — risks & kill criteria

Four ways this goes wrong.

If any of these trigger, pause and re-scope before pushing further. None are likely; all are recoverable.

RISK · waitlist starves

Approvals can't keep up with signups

If we get a big drop and the queue grows faster than the admin's morning batch, the experience reads as "spam, not exclusive." Mitigation: increase weekly batch size, or add a "we'll be in touch this week" line to the join email.

RISK · tweet quality

Tweets that get in are bad ones

Templates are too on-the-nose, users post them verbatim, the feed looks astroturfed. Mitigation: keep templates loose ("starting points, not scripts"), reject low-quality submissions at admin review. Track tweet engagement.

RISK · paid users churn

2-week Tester retention is too short

Tester users hit expiry, find no value in re-scanning, churn. Mitigation: monitor the first cohort closely; if churn > 50%, extend Tester retention to 1 month and re-pitch.

RISK · pricing wrong

The whole price ladder is off

$5 / $10 / $20 / $60 was set by gut. If we discover that buyers anchor on per-scan pricing instead of per-month, the tier shape changes. Mitigation: V1 is admin-granted on purpose — we can flex pricing per conversation before Stripe goes live.

Kill criteria — when to stop

  • If > 30% of approved users never run a scan within 7 days of activation, the onboarding is broken — stop building new tiers, fix the first-scan path.
  • If < 5% of WAITLIST users post a tweet, the jump-line incentive is too weak — strengthen the templates, or accept that the waitlist runs entirely on admin pace.
  • If support load > 1hr / day on the admin's time for the first 30 days, the admin panel is missing affordances — pause feature work and add them.