Purge for Mac: Free, Open-Source Mac Cleaner
Free open-source Mac cleaner for caches, Xcode junk, and app leftovers. Trash-first, no telemetry. Install with Homebrew or download for macOS 13+.
There's a pattern running your life you haven't named yet. Answer five questions and find out what it is, where it started, and where it's still running today.
OtherTypeScript · CSSGitHub @BeyondpsychologyOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
repo pattern spotter
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗A free, email-gated AI reading tool. Visitors answer five questions, pick the hypothesis that lands, and get back a 5-section reading plus product recommendations — one full reading per email address.
/ — placeholder salespage (to be replaced later)/tool — the entire product: email gate → 5 questions → hypothesis
selection → full reading. No login, no accounts.This guide assumes you've never done this before. Follow it in order.
npm install
Copy the example file:
cp .env.local.example .env.local
Then fill in each value in .env.local:
| Variable | Where to get it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | supabase.com → create a free project → Project Settings → API → "Project URL" |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Same page, "anon public" key. (Not currently used by any client-side code in this app, since there's no login — it's included so the project matches the standard Supabase env var setup, in case you add client-side reads later.) |
SUPABASE_SERVICE_ROLE_KEY | Same page, "service_role" key. Keep this secret — it bypasses all database security rules. It is only ever read inside API routes on the server, never sent to the browser. |
ANTHROPIC_API_KEY | console.anthropic.com → API Keys → Create Key. This key is billed per API call — see the cost note at the bottom of this file. |
ACTIVECAMPAIGN_API_URL | ActiveCampaign → Settings → Developer → "API URL". |
ACTIVECAMPAIGN_API_KEY | Same page, "API Key". Used server-side only, to sync each captured email into ActiveCampaign (tag patternspotter, list Pattern Spotter). The Pattern Spotter list must already exist in ActiveCampaign — create it once in the ActiveCampaign dashboard; the tag is created automatically on first use if missing. If either of these two vars is unset, the sync is silently skipped — it never blocks someone from using the tool. |
Never commit .env.local — it's already in .gitignore.
app/api/generate/route.ts in this project and copy the SQL comment
block at the very top of the file (starts with CREATE TABLE IF NOT EXISTS email_captures).This creates one table, email_captures, which tracks which emails have
already used their free reading (has_completed) and stores the reading
itself as JSON for future reference.
npm run dev
Open http://localhost:3000/tool — that's the
whole product. http://localhost:3000/ is just the placeholder salespage.
.env.local: NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
ANTHROPIC_API_KEY, ACTIVECAMPAIGN_API_URL, ACTIVECAMPAIGN_API_KEY.git push to your main branch redeploys automatically./api/generate is the only route that costs money per call (it's the one
Anthropic API request that produces the full reading; /api/hypotheses is
a much cheaper, shorter call). The has_completed flag on email_captures
is what prevents someone from generating more than one free reading per
email address — once it's true for an email, /api/generate returns a 403
without calling Anthropic at all. There's also a simple 20-requests-per-hour
per-IP limit on /api/generate as a backstop against a single source
hammering the tool.
Both PDF paths share the same generator (lib/pdf.ts: title page, styled
sections, a dark CTA page, an about-Beyond-Psychology page), built with
pdfkit — pure JavaScript, no headless browser involved:
/api/generate-pdf, which builds the PDF and streams it straight to the
browser. Nothing is stored for this path./api/generate builds the same PDF, uploads the result
to a private Supabase Storage bucket, and pushes a 30-day signed URL into
an ActiveCampaign custom field for an automation to email out.An earlier version of this rendered the PDF via puppeteer-core +
@sparticuz/chromium (real HTML/CSS, closer to the on-screen design) but
that combination hit two separate Vercel-specific failures in production
(file tracing dropping the bundled Chromium binary, then a missing system
library even after fixing that) and was replaced with this pdfkit version,
which has no native/binary dependency and can't hit that class of bug.
lib/pdf.ts loads the founder photo from public/pdf-assets/myrthe-photo.png
if present, otherwise fetches it from beyondpsychology.eu at render time.
If neither is available, that spot renders as a plain colored circle instead
of failing. Brand fonts (Abril Fatface, Cormorant Garamond italic, Open
Sans) are loaded from public/pdf-assets/fonts/ and registered directly
with pdfkit.
Every completed reading pushes two things to the contact's ActiveCampaign
record, via logReadingTopic in lib/activeCampaign.ts:
pattern-perfectionism-and-overachievement
or pattern-self-sabotage-+-procrastination for a blend. Tags are created
automatically the first time they're used — nothing to set up in AC ahead
of time. Use these to segment your list by pattern for future campaigns.The pattern name itself comes from a new PATTERN_LABEL: line the model
now outputs (see OUTPUT FORMAT in lib/prompts.ts), parsed in
lib/parseReading.ts. It's drawn from the same fixed taxonomy already used
to name the pattern in Section 1 of the reading, so tags stay a small,
consistent set rather than one-off free text.
The tool ships with a full paid-credits system, off by default: while it's off, the tool behaves exactly like the original free, one-reading-per-email version. Nothing changes until you deliberately turn it on.
How it works once turned on: every reading requires a credit. New emails
start at 0 credits — no free reading. The 5 questions and hypothesis
selection are always free to fill in regardless of credits; the paywall only
appears right before the reading itself would be generated (the actual
Anthropic call), when someone has 0 credits at that moment. This is
deliberate — asking for payment after someone has already invested a few
minutes answering the questions converts better than asking upfront. The
paywall (components/tool/BuyAccess.tsx) offers a single pack, configured in
lib/stripe.ts (CREDIT_PACKS): 5 readings for €27. The "X readings left"
count shows on the question form and the reading itself once you have any
credits.
Currency: the charge is always in EUR via Stripe, shown as such everywhere - not localized per visitor.
If someone already answered the questions and picked a hypothesis before
hitting the paywall, their answers + chosen belief are stashed in
localStorage (pattern-spotter:pending-generation, see app/tool/page.tsx)
right before the payment-required attempt. After a successful payment, the
reading is generated immediately from that instead of sending them all the
way back to the first question — the round trip through Stripe's hosted
checkout is a full page navigation, which wipes React state, so this has to
survive in localStorage rather than component state.
Surviving a closed tab, not just a Stripe round trip: separately, as
soon as hypotheses come back, name + email + answers + hypotheses are saved
to localStorage too (pattern-spotter:in-progress-session). If someone
closes the tab while choosing a hypothesis and comes back later, an effect
in app/tool/page.tsx restores that session (re-validating the email via
/api/email-capture first, so credits/payment state stay correct) and
resumes straight at the hypothesis screen instead of sending them all the
way back to the email gate. Cleared the moment a reading is actually shown.
Purely client-side, same as the question-form draft below - never sent to
or stored by us.
Separately, components/tool/QuestionForm.tsx has a "Save my answers as a
text file" link under the submit button, so someone can keep their own copy
of what they wrote - a plain client-side download, not something we store.
Tracking who never converts: anyone who leaves their email but has never
completed a paid reading gets tagged abandoned-before-payment in
ActiveCampaign (tagAbandonedBeforePayment in lib/activeCampaign.ts,
called from app/api/email-capture/route.ts). The tag is removed the moment
they actually complete a paid reading (clearAbandonedBeforePayment, called
from app/api/generate/route.ts), so the segment only ever contains people
who genuinely never converted, not returning customers between purchases.
Build an ActiveCampaign automation on the trigger "tag added:
abandoned-before-payment" (with a wait step, then a check that the tag is
still present before sending) to follow up with this segment. Nothing to
create in ActiveCampaign ahead of time - the tag is created automatically on
first use.
Run this migration once in Supabase's SQL Editor (safe to run anytime, even before turning payments on):
ALTER TABLE email_captures ADD COLUMN IF NOT EXISTS credits_remaining integer not null default 0;
Add these to Vercel's environment variables:
| Variable | Where it comes from |
|---|---|
STRIPE_SECRET_KEY | Stripe Dashboard → Developers → API keys → Standard keys → "Secret key" (sk_live_... for real payments, sk_test_... while testing). Not the "Restricted key" section. |
STRIPE_WEBHOOK_SECRET | Stripe Dashboard → Developers → Webhooks → Add endpoint. URL: https://<your-domain>/api/stripe-webhook. Event to send: checkout.session.completed. After creating it, Stripe shows a "Signing secret" (whsec_...) — that's this value. |
TESTER_COUPON_CODE | Any secret word you pick, e.g. BETATESTER2026. Anyone who enters it in the "Have a code?" field on the email gate gets TESTER_COUPON_CREDITS (5, set in lib/payments.ts) free credits, as if they'd bought the 5-pack — use it to keep letting testers through without charging them. |
PAYMENTS_ENABLED | Leave unset (or false) until you're ready to go live. Set to true and redeploy (Vercel's "Redeploy" button is enough — no code change needed) to flip the whole tool over to paid credits. |
SALE_NOTIFICATION_EMAIL | Optional. Your own inbox — set this to get a "new sale" email each time someone pays. Requires a matching ActiveCampaign field + automation, see below. |
WOOCOMMERCE_API_URL | Optional. https://beyondpsychology.eu — set this (with the two keys below) to also log each sale as a completed order in WooCommerce, see below. |
WOOCOMMERCE_CONSUMER_KEY / WOOCOMMERCE_CONSUMER_SECRET | WooCommerce → Settings → Advanced → REST API → Add key (Permissions: Read/Write). |
Redeploy. PAYMENTS_ENABLED=true is the actual on/off switch; everything
else can be configured ahead of time without affecting live visitors.
lib/payments.ts — the PAYMENTS_ENABLED flag and the coupon-code check.
Every route branches on this flag: unset/false keeps the original
has_completed logic untouched; true switches to credits_remaining.components/tool/BuyAccess.tsx — shown right before generating the reading
when someone has 0 credits, offering the single pack from CREDIT_PACKS.
Buying it calls /api/create-checkout-session with that pack's id,
which creates a Stripe Checkout Session (storing the pack's credit count in
the session's own metadata, not re-derived from price later) and redirects
to Stripe's hosted payment page./api/stripe-webhook — Stripe calls this after a successful payment
(checkout.session.completed) and it adds that pack's credits to that
email (read from the session metadata), fires the Meta Conversions API
Purchase event with the actual amount paid, and (if SALE_NOTIFICATION_EMAIL
is set) pushes a "new sale" notification through ActiveCampaign the same
way the reading PDF gets emailed: create a custom field called
"Pattern Spotter New Sale" in ActiveCampaign, make sure
SALE_NOTIFICATION_EMAIL has a contact there (or let it get created
automatically on the first sale), then build an automation triggered on
that field changing, which emails you the field's value. It also (if
WOOCOMMERCE_API_URL/WOOCOMMERCE_CONSUMER_KEY/WOOCOMMERCE_CONSUMER_SECRET
are set) creates a paid, completed order in WooCommerce via
lib/woocommerce.ts, purely so the sale shows up in the WordPress
dashboard alongside her other orders — no payment actually runs through
WooCommerce, it's a record-only order for the real Stripe charge that
already happened. Get the consumer key/secret from WooCommerce →
Settings → Advanced → REST API on beyondpsychology.eu; WOOCOMMERCE_API_URL
is just the site's base URL (https://beyondpsychology.eu).app/tool/page.tsx reconstructs
everything from the ?checkout=success&email=...&name=... (or
cancelled) query string on the redirect back, then polls
/api/email-capture briefly (the webhook can take a second or two to
land) before continuing.lib/trafficSource.ts captures UTM params, document.referrer, and the
landing URL into sessionStorage on /tool's first load (once per
session, never overwritten by later in-site navigation), and
BuyAccess.tsx sends it along when creating the checkout session. Nothing
is stored server-side — it only ever lands as metadata on the Stripe
Checkout Session and the resulting PaymentIntent, visible directly on
each payment in the Stripe dashboard. Only applies going forward; it
can't retroactively add source data to past payments./review is a standalone page (linked from the bottom of the on-screen
reading, prefilled with the reader's email) where people can leave a star
rating + written review. There's no public review wall — submissions go
straight into a reviews table for you to read and curate, the same way
you already hand-pick testimonials for the salespage.
create table if not exists reviews (
id uuid primary key default gen_random_uuid(),
name text,
email text not null,
rating smallint not null check (rating between 1 and 5),
review_text text not null,
used_on_salespage boolean not null default false,
created_at timestamptz not null default now()
);
reviews table in Supabase's Table
Editor — no separate admin page in the app. Tick used_on_salespage
for any you've copied onto the salespage, so you can tell at a glance
which ones are still unused.SALE_NOTIFICATION_EMAIL. It's reused here as "the owner's
own notification inbox" rather than something sale-specific.Supabase's free tier auto-pauses a project after 7 days with no API/database
activity — visitors get a 500 error until someone notices and manually
restores it in the Supabase dashboard. vercel.json runs a daily Vercel
Cron job that hits /api/keep-alive (a trivial one-row read, retried a few
times on transient failures), which counts as activity and resets that
7-day clock, so a quiet week never takes the tool down silently.
The Vercel Cron alone wasn't reliable enough in practice — the project still
got auto-paused more than once despite it. .github/workflows/keep-alive.yml
adds a second, independent daily ping (GitHub's scheduler instead of
Vercel's, 12 hours apart from the Vercel cron) at the same /api/keep-alive
URL, so one scheduler having an off day doesn't matter.
This is a stopgap. Once real payments are flowing, upgrade the Supabase
project to a paid plan instead — those never auto-pause, and at that point
an unexpected pause is a lot more costly than $25/month. When that happens,
/api/keep-alive, the cron entry in vercel.json, and
.github/workflows/keep-alive.yml can all be deleted.
Members of the separate WordPress/WooCommerce membership site get into the tool for free, bypassing the credits/one-free-reading gate entirely, via a signed link WordPress generates when an active member clicks "Open Pattern Spotter" — not a login, and nothing synced into this tool's own database.
https://patternspotter.beyondpsychology.eu/tool?email=<email>&name=<name>&exp=<unix seconds, now+300>&token=<hex HMAC-SHA256(secret, "email|name|exp")>.MEMBERSHIP_LINK_SECRET is the shared secret — set the same value in
WordPress (wp-config.php on their side) and in this project's Vercel
environment variables./api/membership-login (called once, when /tool loads
with these params) checks that link once, and if it's valid, mints this
tool's own longer-lived session token (2 hours, see
lib/membershipAuth.ts). That second token is what actually travels with
the /api/generate call for the rest of the visit, not the original
WordPress link — both are HMAC-signed, nothing is ever trusted as a bare
client-side flag.app/tool/page.tsx skips the email gate entirely when these link params
are present and verify, and jumps straight to the questions.app/api/generate/route.ts skips both the credits check (paid-credits
mode) and the one-free-reading check (free mode) when a valid session
token is present, and never decrements or writes credits_remaining for
that request.pattern-spotter-member in ActiveCampaign on their
first reading (tagMember in lib/activeCampaign.ts, created
automatically on first use), so the list can be segmented separately from
paying or free-tier visitors.app/
page.tsx salespage placeholder
tool/page.tsx the entire tool (client-side state machine)
api/
email-capture/route.ts stage 1: check/insert email, no AI call
hypotheses/route.ts stage 2→3: 2-3 hypotheses from Anthropic
generate/route.ts stage 3→4: full reading, has_completed check + update
generate-pdf/route.ts on-demand PDF download (pdfkit, not stored)
create-checkout-session/route.ts Stripe Checkout session (paid mode only)
stripe-webhook/route.ts grants credits on successful payment
components/tool/ UI pieces for each stage
components/SiteHeader.tsx logo header, shown on every page via layout.tsx
lib/ prompts, product catalog, supabase/rate-limit/AC/PDF helpers
public/pdf-assets/ fonts + photo used only by the PDF template
Start at app/tool/page.tsx to see how the four stages fit together, then
app/api/generate/route.ts for the core logic (the rate limit, the
has_completed check, the Anthropic call, and the response parsing).