Online Tools is a Next.js 16 application with browser-first utilities for text, developer, image, video, and PDF workflows. Tool processing happens in the browser. The backend is only used for report intake, simple aggregate analytics, and an admin view.
- Current Status
- Tool Catalog
- Local Development
- Production Build Security (Source Maps)
- Testing
- SEO
- Environment Variables
- Supabase Setup
- Cloudflare Turnstile Setup
- Production Data Persistence
- Deploy to Vercel + Cloudflare (Custom Domain)
- Production Checklist
- Routes
- Project Structure
- Framework: Next.js App Router + TypeScript + React 19.
- Tools shipped: 78.
- Tool execution model: client-side only.
- Backend data storage: Supabase-hosted Postgres for analytics + report intake.
- Auth + tool state: Supabase auth sessions + cross-device favorites + per-tool upvotes.
- Bot protection: Cloudflare Turnstile on login/signup.
- HEIC to JPG/PNG:
- Reworked HEIC decode path to avoid headless/browser worker deadlocks and restore reliable selected-file previews plus conversion output/download flow.
tests/e2e/heic-to-jpg.spec.tsandtests/e2e/image-thumbnail-regression.spec.tsnow pass with existing assertion intent.
- Video happy-path coverage:
- Added concrete
ACTIONSentries for:mp4-to-mov,mkv-to-mp4,webm-to-mp4,avi-to-mp4mp4-to-mp3,mp4-to-gifvideo-compressor,video-trimmer,rotate-flip-video,resize-video
- Added shared tiny-video fixture generation helpers in
tests/e2e/tools-happy.spec.ts(real upload + run + output assertions).
- Added concrete
- Tool upvotes (Supabase-backed):
- Added reusable
ToolUpvotecomponent on shared tool pages. - Added shared client utility for loading count/state + RPC toggle.
- Added Supabase migration for
tool_upvotes,tool_vote_counts, trigger sync, summary view, andtoggle_tool_upvote()RPC with RLS policies.
- Added reusable
- New SEO-intent wrappers and pages:
- Image:
webp-to-jpg,webp-to-png,jpg-to-png,png-to-jpg,jpeg-to-png,png-to-ico,compress-image,bulk-image-converter. - PDF:
pdf-to-jpg,jpg-to-pdf,compress-pdf-online,pdf-crop. - Developer:
sql-formatter,cron-generator,json-schema-generator.
- Image:
- Wrapper architecture:
- Added shared image-intent converter wiring (batch conversion + ZIP flow) that reuses existing browser conversion logic.
- Extended
pdf-to-imagespipeline to support JPG output and wrapper defaults.
- SEO/content pass:
- Strengthened unique metadata for image converters and new intent pages (title/description/keywords).
- Added explicit searchable-PDF CTA/internal linking on relevant OCR/PDF extraction pages instead of introducing unsupported server OCR-to-PDF behavior.
- Kept structured data pattern consistent (
WebApplication+FAQPage) with canonical URLs on all tool pages.
- Added new PDF workflows:
edit-pdf(annotation MVP: draw/highlight/text + per-page undo/clear + export).protect-pdfandunlock-pdfusing browser-side encryption/decryption (@libpdf/core) with lazy loading.pdf-to-pngandpng-to-pdfintent wrappers.pdf-to-pptx(one rendered PDF page image per slide).pptx-to-pdf(render slides to images, then package into PDF pages).searchable-pdf(OCR + invisible text layer over image-backed pages).
- Added image/developer utilities:
crop-image(interactive crop rectangle with PNG/JPG export).website-status-checker(browser HTTP reachability + latency probe stats).
- Added e2e smoke coverage for new tool routes,
/llms.txtslug inclusion, and download sanity checks for annotation/OCR PDF output.
- Header/auth consistency:
- Normalized Login/Sign up/Logout actions in the top nav and aligned button/link styling.
- Favorites UX:
- Improved Favorites filter empty states with sheep-accented copy and clearer sign-in/account CTAs.
- Added clearer favorite toggle affordance on tool pages (star glyph + explicit labels).
- Conversion messaging:
- Added non-intrusive free-account value callouts on home and tool pages (unlimited runs + synced favorites).
- SEO improvements:
- Strengthened route metadata for home, tool pages, about, login, and signup.
- Improved OG/Twitter image text branding.
- Extended structured data with
WebSite+Organizationon home/about while keeping toolWebApplication+FAQPage.
- About page:
- Added username-only auth disclaimers (no email verification, no password recovery currently, password manager recommendation).
- Added privacy notes for local browser processing, Supabase account/favorites storage, and Turnstile verification.
- Word to PDF:
- Added Visual Render mode (fidelity-first) that includes embedded DOCX images and best-effort HTML layout before PDF export.
- Kept Text Mode for selectable text output with Unicode-capable font embedding to avoid WinAnsi failures on symbols and smart punctuation.
- Added fallback glyph replacement with non-blocking warning when unsupported characters are substituted.
- Updated messaging to clarify Visual Render (image-based PDF) vs Text Mode tradeoffs.
- PDF Extract:
- Added first-class thumbnail page selection (click, Shift+click range, Ctrl/Cmd+click additive).
- Added
Select all/Clear selectioncontrols while retaining page-range input support. - Shared thumbnail selection logic with PDF Rotate to keep behavior consistent.
- New tool:
pptx-to-imagesnow renders embedded slide images (best effort) in addition to text and exports PNG/WebP with ZIP download.- Updated PPTX selected-file UI to the same compact file-chip/card style used in Word to PDF.
- PDF thumbnail-heavy workflows:
- Added a reusable sticky output/download action bar for long-page thumbnail tools so download CTA remains reachable without scrolling to the bottom.
- Includes compact output-ready summary and quick navigation actions (scroll to thumbnails / back to top).
- Header favorites icon:
- Simplified to a clean professional star icon (removed sheep accent) while keeping active state and accessibility behavior.
- New PDF tool:
- Added
pdf-to-excelwith page-thumbnail selection, Shift/Ctrl multi-select, range support, and XLSX export (one sheet per page or single appended sheet). - Added best-effort text-grid/table heuristics and warnings when PDFs have no selectable text (with OCR CTA).
- Added
- New image tool:
- Added
heic-to-jpgwith HEIC/HEIF batch conversion to JPG or PNG, quality controls, optional resize, metadata stripping, and ZIP export.
- Added
- New OCR tool:
- Added
ocr-to-textfor JPG/PNG/WebP and PDFs with in-browser OCR, PDF thumbnail page selection, progress reporting, and TXT/ZIP output modes.
- Added
case-converter- Convert text between upper, lower, title, sentence, camel, snake, and kebab cases.whitespace-cleaner- Trim, normalize, and clean whitespace/newline formatting in text.word-counter- Count words, characters, sentences, paragraphs, reading time, and top keywords.lorem-ipsum-generator- Generate placeholder text by words, sentences, or paragraphs with themed presets.ocr-to-text- Extract text from images and scanned PDFs in-browser with thumbnail page selection for PDF mode.
json-formatter- Format, minify, and validate JSON.sql-formatter- Beautify/minify SQL queries with dialect, case, and indent controls.cron-generator- Build cron expressions with presets and human-readable schedule summaries.json-schema-generator- Generate JSON Schema from JSON input with optional required/example fields.url-encode- URL encode/decode text.base64-encode- Base64 encode/decode text.hash-generator- Generate SHA-256, SHA-1, and MD5 hashes.uuid-generator- Generate v4 UUIDs in bulk.password-generator- Generate random passwords with configurable character sets.jwt-decoder- Decode JWT header/payload (no signature verification).csv-json- Convert CSV <-> JSON.yaml-json- Convert YAML <-> JSON.html-css-minifier- Minify HTML and CSS snippets.xml-formatter- Format/minify XML with parse validation and indentation controls.html-formatter- Beautify HTML with configurable indentation and line wrapping.regex-tester- Test JavaScript regex patterns against sample text.timestamp-converter- Convert epoch and date/time strings across UTC, local, and custom offsets.diff-checker- Compare two texts in unified or side-by-side diff modes.url-parser- Parse URL components, edit query parameters, and rebuild URLs.website-status-checker- Run browser-based HTTP reachability probes with latency min/avg/max and jitter stats.qr-generator- Generate downloadable QR codes as PNG/SVG from text or URLs.
image-resize- Resize/compress JPG, PNG, and WebP images with EXIF removal.image-convert- Convert JPG/PNG/WebP with optional resize and metadata stripping.crop-image- Interactively crop JPG/PNG/WebP with drag/resize selection and PNG/JPG export.bulk-image-converter- Batch convert JPG/PNG/WebP with resize/quality controls and ZIP export.compress-image- Intent wrapper for fast image compression workflows.avif-to-png- Convert one or many AVIF files to PNG/JPG with single or ZIP download.heic-to-jpg- Convert one or many HEIC/HEIF files to JPG (default) or PNG with optional resize and ZIP download.jfif-to-jpg- Convert one or many JFIF files to JPG/PNG with ZIP download.webp-to-jpg- Convert WEBP files to JPG (default) with batch + ZIP output.webp-to-png- Convert WEBP files to PNG output with batch + ZIP support.jpg-to-png- Convert JPG/JPEG files to PNG in batch.png-to-jpg- Convert PNG files to JPG with quality controls.jpeg-to-png-.jpeg-focused PNG conversion page with distinct SEO intent content.png-to-ico- Convert PNG files into favicon-style ICO output (16/32/48 entries).svg-to-png- Convert one or many SVG files to PNG/JPG/WebP with scale and background controls.favicon-generator- Generate a favicon pack (favicon.ico, PNG icon set, andsite.webmanifest) from one source image.pptx-to-images- Convert PPTX slides to PNG/WebP images in-browser with quality scaling and ZIP download.color-picker- Pick image colors, extract dominant palettes, and export HEX/RGB/HSL/CSS variables.exif-viewer- Inspect EXIF/IPTC metadata fields including GPS when available.exif-stripper- Re-encode images to strip metadata and export cleaned files.
mov-to-mp4- Convert.movto.mp4locally in-browser using Auto mode (remux first, fallback to H.264/AAC transcode).
pdf-merge- Merge multiple PDFs.pdf-split- Split by range or every N pages.pdf-rotate- Rotate selected pages.pdf-extract- Extract selected pages into a new file.pdf-reorder- Reorder pages with drag-and-drop and export a new PDF.pdf-delete-pages- Remove selected pages by thumbnail/range and export updated PDF.pdf-watermark- Add configurable text watermarks (position/opacity/rotation/range).pdf-page-numbers- Add page numbers with custom format, start index, and placement.pdf-metadata- View/edit/remove metadata fields and save updated PDF.images-to-pdf- Build a PDF from multiple images with page layout options.png-to-pdf- PNG-first wrapper for image-to-PDF conversion (also accepts JPG/WebP).jpg-to-pdf- JPG-first image-to-PDF wrapper (also accepts PNG/WebP).pdf-to-images- Render PDF pages to PNG/WebP and download as ZIP.pdf-to-png- PDF-to-PNG intent wrapper with PNG default and optional WebP output.pdf-to-jpg- PDF to JPG wrapper with JPG-default export intent.pdf-to-pptx- Convert PDF pages to PPTX with one rendered page image per slide.pptx-to-pdf- Convert PPTX slides to an image-based PDF (one slide image per page).pdf-to-word- Convert PDF to DOCX using text-first extraction or page-image mode.word-to-pdf- Convert DOCX to PDF with Visual Render mode (images + layout fidelity) or Text Mode (selectable text).pdf-to-text- Extract selectable text from PDF with per-page and layout-spacing options.searchable-pdf- OCR scanned pages/images and export an image-backed PDF with an invisible text layer.pdf-to-excel- Convert selectable PDF text into XLSX using text-layout or table-heuristic extraction with page selection controls.pdf-compress- Best-effort compression by rasterizing pages at configurable DPI/quality presets.compress-pdf-online- Intent wrapper targeting “compress PDF online” with the same local compression pipeline.pdf-crop- Crop page margins (top/right/bottom/left) and rebuild cropped PDFs client-side.pdf-extract-images- Extract embedded PDF images with page-image fallback and ZIP export.pdf-sign- Place a drawn/uploaded visual signature onto a selected PDF page and export.pdf-flatten- Flatten PDFs by rasterizing pages into a non-editable output document.edit-pdf- Annotate PDFs with draw/highlight/text tools and export a rebuilt annotated PDF.protect-pdf- Add password protection and permissions to a PDF with browser-side encryption.unlock-pdf- Remove PDF protection when credentials allow; includes image-based fallback for hard-to-decrypt files.
- PDF hard limit: 20 MB max per PDF file.
pdf-merge: up to 20 files and 200 pages total.pdf-split,pdf-rotate,pdf-extract,pdf-reorder,pdf-delete-pages,pdf-watermark,pdf-page-numbers,pdf-metadata,pdf-to-images,pdf-to-jpg,pdf-to-png,pdf-to-text,pdf-to-pptx,pdf-compress,compress-pdf-online,pdf-crop,pdf-extract-images,pdf-sign,pdf-flatten,protect-pdf,unlock-pdf: up to 100 pages.edit-pdf: up to 80 pages (annotation-preview performance cap).pdf-to-excel: up to 50 pages (lower cap due extraction cost).images-to-pdf/jpg-to-pdf/png-to-pdf: up to 20 images (10 MB each, max 100 output pages).- Image conversion/compression/edit tools (
image-resize,image-convert,crop-image,bulk-image-converter,compress-image,avif-to-png,heic-to-jpg,jfif-to-jpg,webp-to-jpg,webp-to-png,jpg-to-png,png-to-jpg,jpeg-to-png,png-to-ico,exif-viewer,exif-stripper): 10 MB max per file (batch converters support up to 20 files unless stated otherwise). ocr-to-text:- Images: up to 20 files, 10 MB each.
- PDF mode: one PDF, 20 MB max, up to 30 pages per run.
searchable-pdf:- PDF mode: one PDF, 20 MB max, up to 20 pages per run.
- Image mode: up to 20 images, 10 MB each.
pptx-to-images/pptx-to-pdf: 25 MB max per PPTX and up to 80 slides.mov-to-mp4: one.movper run, 500 MB max on desktop, 100 MB max on mobile. Auto mode attempts remux first, then transcodes to H.264/AAC if remux is incompatible.- Typical text-tool limits range from 20,000 to 1,000,000 characters, depending on tool metadata.
- Usage limits:
- Guests:
NEXT_PUBLIC_GUEST_RUNS_PER_TOOL(default5) per tool. - Authenticated users:
NEXT_PUBLIC_AUTH_RUNS_PER_TOOL(defaultInfinity) per tool.
- Guests:
- Registry-driven tool system (
src/tools/registry.ts) with lazy loading (src/tools/loader.ts). - Shared reusable UI components for tool screens (
src/components/*). - Search + category filtering on the home page (Text / Developer / Image / Video / PDF).
- Favorites filter in the home explorer for signed-in users.
- Per-tool metadata for SEO and discoverability.
- Username-only Supabase auth (
/login,/signup) with cookie-backed sessions via@supabase/ssr. - Cloudflare Turnstile verification on signup/login before Supabase auth calls (with official deterministic test keys for local dev).
- Optional server-side auth rate limiting (
/api/auth/signupand/api/auth/login) with Upstash Redis (disabled by default). - Shared usage limiter boundary for tool runs (
src/lib/usageLimiter.ts+ shared run buttons). - Report flow at
/reportbacked by server-side validation. - Admin dashboard at
/adminfor reports and usage counters.
- Node.js 20+ recommended.
- npm (project uses
package-lock.json).
npm install
npm run devOpen http://localhost:3000.
npm run dev # start dev server
npm run build # production build
npm run build:secure # production build + sourcemap/env-exposure assertions
npm run assert:env-safety # fail on client/service-role env exposure patterns
npm run start # run production build
npm run lint # lint codebase
npm run test # run unit/integration tests (Jest)
npm run test:watch
npm run test:e2e # run Playwright browser tests
npm run test:all # run unit + e2e
npm run seed:test-users # create and validate 10 dev auth users via API (no deletion)
npm run test:rls:dev # verify cross-user favorite isolation in dev- Production browser source maps are explicitly disabled in
next.config.ts. - Server source maps are explicitly disabled in
next.config.ts. - Production client webpack config is hardened (
devtool = false) so source maps are not emitted in non-dev client builds. - Production builds remain minified via Next.js defaults (no heavy obfuscation tooling added).
- CI and local hardening command:
npm run build:secure(npm run build+scripts/assert-no-sourcemaps.mjs+scripts/assert-env-safety.mjs). scripts/assert-env-safety.mjsfails if:- a
NEXT_PUBLIC_*SERVICE_ROLE*env name exists, SUPABASE_SERVICE_ROLE_KEYappears in a client component,- a service-role key name/value appears in
.next/staticclient bundle output.
- a
- No custom legacy-polyfill/browserslist target overrides are applied; keeping Next.js defaults avoids risky compatibility regressions.
- Requests for
*.mapshould return404by default because map files are not generated/deployed. - If error-tracking integration is added later, source maps must be uploaded privately only and never served publicly. Remove local map files before deployment.
- Unit/integration tests:
npm run test - Watch mode:
npm run test:watch - E2E browser tests:
npm run test:e2e - Full suite:
npm run test:all - Vercel Analytics sanity check: deploy to Vercel, visit the live site, then confirm traffic appears in the project
Analyticstab. - Vercel Speed Insights sanity check: deploy to Vercel, generate real traffic, then confirm data appears in the project
Speed Insightstab.
- App Router Metadata API is used for global metadata defaults and per-route canonical URLs.
src/app/robots.tsandsrc/app/sitemap.tsexpose crawl directives and tool-page discovery.- Every tool page includes visible "About this tool" content, FAQ content, related internal links, and matching JSON-LD (
WebApplication+FAQPage). - Home page includes server-rendered category sections and crawlable tool links.
Validation workflow:
- Run Lighthouse against
/and a few/<slug>pages. - Validate rich snippets with Google's Rich Results Test.
- Inspect canonical/metadata tags in page source and browser devtools.
Notes:
- E2E fixtures are generated automatically in
tests/fixturesby Playwright global setup. - If Playwright browsers are missing, run
npx playwright install chromium.
Set these in .env.local for local development and in Vercel Project Settings -> Environment Variables for production.
ADMIN_USERNAME- Admin login username. Default:admin.ADMIN_PASSWORD- Admin login password. Default:admin.ADMIN_SESSION_SECRET- HMAC secret used to sign admin cookie.NEXT_PUBLIC_SITE_URL- Canonical public site URL (metadata,robots.txt, andsitemap.xml), for examplehttps://tools.baabaasheep.party.CANONICAL_HOST- Optional canonical host redirect target (middleware 308 host redirects), for exampletools.baabaasheep.party.NEXT_PUBLIC_COMMIT_SHA- Optional commit SHA shown on home page footer.NEXT_PUBLIC_COMMIT_MESSAGE- Optional commit message shown on home page footer.
NEXT_PUBLIC_SUPABASE_URL- Supabase project URL.NEXT_PUBLIC_SUPABASE_ANON_KEY- Supabase anon/public key.SUPABASE_SERVICE_ROLE_KEY- Server-only key. Required for server-side analytics writes (/api/analytics) and report writes/admin report reads (/api/reports,/api/admin/reports). Also used for optional signup rollback cleanup.NEXT_PUBLIC_TURNSTILE_SITE_KEY- Cloudflare Turnstile site key (public).TURNSTILE_SECRET_KEY- Cloudflare Turnstile secret key (server-only; never expose).TURNSTILE_ENABLED- Optional toggle (true/false). Default:true.AUTH_RATE_LIMIT_ENABLED- Optional toggle for auth endpoint rate limiting. Default:false.UPSTASH_REDIS_REST_URL- Upstash Redis REST URL for auth endpoint rate limiting.UPSTASH_REDIS_REST_TOKEN- Upstash Redis REST token for auth endpoint rate limiting.NEXT_PUBLIC_GUEST_RUNS_PER_TOOL- Guest limit per tool. Default:5.NEXT_PUBLIC_AUTH_RUNS_PER_TOOL- Logged-in limit per tool. Default:Infinity.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED- Optional toggle for Google OAuth button. Default:false.
Canonical + SEO:
-
NEXT_PUBLIC_SITE_URL(required) -
CANONICAL_HOST(optional but recommended) -
NEXT_PUBLIC_COMMIT_SHA(optional) -
NEXT_PUBLIC_COMMIT_MESSAGE(optional)
Admin:
-
ADMIN_USERNAME -
ADMIN_PASSWORD -
ADMIN_SESSION_SECRET
Supabase:
-
NEXT_PUBLIC_SUPABASE_URL -
NEXT_PUBLIC_SUPABASE_ANON_KEY -
SUPABASE_SERVICE_ROLE_KEY(server-only; required for analytics writes and report write/admin-read paths)
Turnstile:
-
NEXT_PUBLIC_TURNSTILE_SITE_KEY -
TURNSTILE_SECRET_KEY -
TURNSTILE_ENABLED
Optional auth rate limiting:
-
AUTH_RATE_LIMIT_ENABLED -
UPSTASH_REDIS_REST_URL -
UPSTASH_REDIS_REST_TOKEN
Limits:
-
NEXT_PUBLIC_GUEST_RUNS_PER_TOOL -
NEXT_PUBLIC_AUTH_RUNS_PER_TOOL -
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED
- Set
NEXT_PUBLIC_GUEST_RUNS_PER_TOOLand/orNEXT_PUBLIC_AUTH_RUNS_PER_TOOLin.env.local. - Restart the dev server.
- Limits are parsed from
src/lib/config.tsand enforced throughsrc/lib/usageLimiter.ts.
How limits are resolved in code:
src/lib/config.tsparses public run-limit flags and defaults.src/lib/config.server.tsholds server-only secrets.- Shared limiter logic lives in
src/lib/usageLimiter.ts.
Important: change default admin credentials and set a strong ADMIN_SESSION_SECRET before any shared deployment.
- Create a Supabase project.
- In Supabase project settings, copy:
- Project URL ->
NEXT_PUBLIC_SUPABASE_URL - Project API anon key ->
NEXT_PUBLIC_SUPABASE_ANON_KEY - Service role key ->
SUPABASE_SERVICE_ROLE_KEY
- Project URL ->
- Add values to
.env.local(local) and Vercel env vars (production). - Restart the dev server.
This repository includes:
supabase/config.tomlsupabase/migrations/20260218123000_create_auth_profiles_and_tool_favorites.sqlsupabase/migrations/20260219094000_create_tool_usage_analytics.sqlsupabase/migrations/20260220103000_create_reports.sqlsupabase/migrations/20260309093000_create_tool_upvotes.sql
Use the Supabase CLI against your hosted project:
- Install CLI (either option):
npm i -D supabase(then usenpx supabase ...)- Install Supabase CLI globally from official Supabase docs.
- Authenticate:
npx supabase login
- Link this repo to your hosted project:
npx supabase link --project-ref <ref>
- Apply migrations:
npx supabase db push
- Optional alternative:
npx supabase migration up --linked
<ref> is your Supabase project reference from the hosted project dashboard.
If you are not using CLI migrations, run this SQL manually in Supabase SQL Editor:
create extension if not exists pgcrypto;
create table if not exists public.profiles (
user_id uuid primary key references auth.users(id) on delete cascade,
username text not null unique,
created_at timestamptz default now()
);
create table if not exists public.tool_favorites (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
tool_id text not null,
created_at timestamptz default now(),
unique (user_id, tool_id)
);
alter table public.profiles enable row level security;
alter table public.tool_favorites enable row level security;
drop policy if exists "profiles_select_own" on public.profiles;
create policy "profiles_select_own"
on public.profiles
for select
using (user_id = auth.uid());
drop policy if exists "profiles_insert_own" on public.profiles;
create policy "profiles_insert_own"
on public.profiles
for insert
with check (user_id = auth.uid());
drop policy if exists "profiles_update_own" on public.profiles;
create policy "profiles_update_own"
on public.profiles
for update
using (user_id = auth.uid())
with check (user_id = auth.uid());
drop policy if exists "tool_favorites_select_own" on public.tool_favorites;
create policy "tool_favorites_select_own"
on public.tool_favorites
for select
using (user_id = auth.uid());
drop policy if exists "tool_favorites_insert_own" on public.tool_favorites;
create policy "tool_favorites_insert_own"
on public.tool_favorites
for insert
with check (user_id = auth.uid());
drop policy if exists "tool_favorites_delete_own" on public.tool_favorites;
create policy "tool_favorites_delete_own"
on public.tool_favorites
for delete
using (user_id = auth.uid());Do not create public policies on either auth/favorites table.
Tool upvotes are defined in:
supabase/migrations/20260309093000_create_tool_upvotes.sql
That migration adds:
public.tool_upvotes(one vote pertool_slug + user_id)public.tool_vote_counts(materialized counts)- trigger sync function (
sync_tool_vote_count) for inserts/deletes public.toggle_tool_upvote(text)RPC (authenticated toggle)public.tool_upvote_summaryview (anon/auth readable counts)- RLS policies so authenticated users can only read/write their own rows in
tool_upvotes
No new environment variables are required beyond existing Supabase variables:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY
- Apply latest Supabase migrations:
npx supabase db push
- Start the app:
npm run dev
- Unauthenticated check:
- Open any tool page.
- Confirm
data-testid="tool-upvote-count"anddata-testid="tool-upvote-button"render. - Click upvote and confirm sign-in prompt/error appears.
- Authenticated toggle check:
- Sign in as user A and click upvote once (count increments, button becomes active).
- Click again (count decrements, vote removed).
- Persistence + isolation check:
- Sign in as user B on the same tool and verify user B sees independent upvote state.
- Switching back to user A should preserve user A's vote state only.
- Optional SQL checks in Supabase SQL editor:
select tool_slug, upvotes_count from public.tool_vote_counts order by updated_at desc limit 20;select tool_slug, user_id, created_at from public.tool_upvotes order by created_at desc limit 20;
Reports persistence is defined in:
supabase/migrations/20260220103000_create_reports.sql
That migration creates public.reports with:
- category check constraint (
Bug/Feature/Security/Other) - title/description length checks (
120/4000) - optional
tool_id,page_path,user_id, anduser_agentcolumns (with length caps) - indexes on
created_at descandtool_id - RLS enabled with
anon/authenticatedrevoked forselect/insert/update/delete service_rolegrantedinsert+selectfor server-only report writes and admin reads
Analytics schema in this repo is intentionally minimal and does not store tool input content.
Security model used by the app:
- RLS is enabled on analytics tables.
anonandauthenticatedcanSELECTtotals/daily rows only.- Public writes are blocked.
/api/analyticswrites server-side usingSUPABASE_SERVICE_ROLE_KEYand theincrement_tool_usageRPC.
SUPABASE_SERVICE_ROLE_KEY is required for privileged server-side analytics writes, report writes, admin report reads, and optional auth rollback cleanup. It must never be available to browser code.
Current server-only usage in this repo:
src/lib/config.server.tsreadsSUPABASE_SERVICE_ROLE_KEY(server-only module).src/lib/supabase/admin.tscreates the admin Supabase client.src/lib/server/analyticsStore.tscallsincrement_tool_usageand reads analytics totals.src/lib/server/reportsStore.tswrites and readspublic.reports.src/app/api/analytics/route.tsis the write/read API boundary for aggregate analytics.src/app/api/reports/route.tsis the report write API boundary.src/app/api/admin/reports/route.tsis the admin report read API boundary.src/app/api/auth/signup/route.tsoptionally uses admin client for rollback cleanup only.
Why this is safe:
- Service-role access is only instantiated from server-only modules (
server-onlyguarded imports). - Client components do not import
config.serveror admin Supabase client modules. - Analytics POST validates slug against the canonical tool registry and enforces server-side rate limiting.
- Database permissions restrict privileged report/analytics writes (and report reads) to
service_role;anon/authenticateddo not have report-table access.
How to confirm it is not exposed:
rg -n "SUPABASE_SERVICE_ROLE_KEY" src scripts testsrg -n "NEXT_PUBLIC_[A-Z0-9_]*SERVICE_ROLE" src scripts tests- Check
.env*files do not define anyNEXT_PUBLIC_*SERVICE_ROLE*variable names. - Run
npm run build:secureand confirmscripts/assert-env-safety.mjspasses. - (Optional) manually inspect
.next/staticoutput to ensure noSUPABASE_SERVICE_ROLE_KEY/sb_secret_tokens appear.
Run this SQL in Supabase SQL Editor (or apply it as a migration file):
create table if not exists public.tool_usage_totals (
tool_id text primary key,
total_runs bigint not null default 0,
updated_at timestamptz not null default now(),
constraint tool_usage_totals_total_runs_nonnegative check (total_runs >= 0)
);
create table if not exists public.tool_usage_daily (
tool_id text not null,
day date not null,
runs bigint not null default 0,
updated_at timestamptz not null default now(),
primary key (tool_id, day),
constraint tool_usage_daily_runs_nonnegative check (runs >= 0)
);
create index if not exists tool_usage_daily_day_idx on public.tool_usage_daily (day);
alter table public.tool_usage_totals enable row level security;
alter table public.tool_usage_daily enable row level security;
grant select on table public.tool_usage_totals to anon, authenticated;
grant select on table public.tool_usage_daily to anon, authenticated;
revoke insert, update, delete on table public.tool_usage_totals from anon, authenticated;
revoke insert, update, delete on table public.tool_usage_daily from anon, authenticated;
drop policy if exists "tool_usage_totals_select_public" on public.tool_usage_totals;
create policy "tool_usage_totals_select_public"
on public.tool_usage_totals
for select
using (true);
drop policy if exists "tool_usage_daily_select_public" on public.tool_usage_daily;
create policy "tool_usage_daily_select_public"
on public.tool_usage_daily
for select
using (true);
create or replace function public.increment_tool_usage(p_tool_id text, p_day date default current_date)
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
normalized_tool_id text;
target_day date;
begin
normalized_tool_id := nullif(trim(p_tool_id), '');
if normalized_tool_id is null then
raise exception 'p_tool_id is required';
end if;
target_day := coalesce(p_day, current_date);
insert into public.tool_usage_totals (tool_id, total_runs, updated_at)
values (normalized_tool_id, 1, now())
on conflict (tool_id) do update
set total_runs = public.tool_usage_totals.total_runs + 1,
updated_at = now();
insert into public.tool_usage_daily (tool_id, day, runs, updated_at)
values (normalized_tool_id, target_day, 1, now())
on conflict (tool_id, day) do update
set runs = public.tool_usage_daily.runs + 1,
updated_at = now();
end;
$$;
revoke all on function public.increment_tool_usage(text, date) from public;
revoke all on function public.increment_tool_usage(text, date) from anon;
revoke all on function public.increment_tool_usage(text, date) from authenticated;
grant execute on function public.increment_tool_usage(text, date) to service_role;How to apply:
- Option 1 (recommended): keep SQL in migration files under
supabase/migrations/and runnpx supabase db push. - Option 2: paste SQL into Supabase SQL Editor and run once per environment.
- Open
Authentication -> Sign In / Providers. - Set
Confirm emailtoOFF. - Save.
If this remains enabled, username-only signup cannot complete and the API returns email_confirmation_enabled.
In Authentication -> URL Configuration:
- Set
Site URLto your production domain. - Add allowed redirect URLs:
http://localhost:3000https://your-production-domain
- Do not use wildcard redirects in production.
- In SQL editor, confirm RLS flags:
select tablename, rowsecurity from pg_tables where schemaname = 'public' and tablename in ('profiles', 'tool_favorites', 'tool_usage_totals', 'tool_usage_daily', 'reports');
- In
Authentication -> Policies, confirm owner-scoped policies for auth/favorites, read-only public policies for analytics tables, and no anon/authenticated access policies onpublic.reports. - Run the dev script
npm run test:rls:devto validate favorites cross-user isolation end-to-end.
- Create a Turnstile widget in Cloudflare.
- Add allowed hostnames:
localhost127.0.0.1- your production hostname
- Set keys in
.env.local:NEXT_PUBLIC_TURNSTILE_SITE_KEYTURNSTILE_SECRET_KEY
- Keep
TURNSTILE_ENABLED=true(default) unless intentionally disabling for local debugging.
Local deterministic testing (official Cloudflare test keys):
- Always-pass site key:
1x00000000000000000000AA - Always-pass secret key:
1x0000000000000000000000000000000AA - Optional always-fail site key:
2x00000000000000000000AB - Optional always-fail secret key:
2x0000000000000000000000000000000AB
Example .env.local test-key block:
NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
TURNSTILE_ENABLED=trueDeterministic dev behavior:
- In non-production only, when
TURNSTILE_SECRET_KEYequals Cloudflare's official always-pass test secret, server validation accepts any non-emptyturnstileToken. - Production remains strict and always calls Cloudflare verification.
- Turnstile remains the primary bot protection while auth rate limiting is disabled by default.
Current default:
AUTH_RATE_LIMIT_ENABLED=false/api/auth/signupand/api/auth/logindo not enforce rate limits.
To enable in a future deployment:
- Set
AUTH_RATE_LIMIT_ENABLED=true. - Set
UPSTASH_REDIS_REST_URL. - Set
UPSTASH_REDIS_REST_TOKEN.
When enabled, limits are:
- Signup: 3 requests/hour/IP.
- Login: 10 requests/15 minutes/IP.
If enabled without Upstash env vars, auth endpoints fail closed with error.code = "rate_limit_unavailable".
Google OAuth is scaffolded and disabled by default.
- In Supabase Auth -> Providers, enable Google.
- Configure Google client ID/secret in Supabase provider settings.
- Add redirect URLs to Supabase and Google console allowlists:
- Local:
http://localhost:3000/auth/callback - Production:
https://your-domain/auth/callback - Vercel previews: allow your preview domain pattern and callback path.
- Local:
- Set
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=true.
Code path:
- Client button uses
supabase.auth.signInWithOAuth({ provider: "google" }). - Callback handler:
src/app/auth/callback/route.ts.
This project includes a dev-only API-driven seed script:
- Command:
npm run seed:test-users - Script:
scripts/seed-test-users.mjs - API endpoint used:
POST /api/auth/signup
Expected behavior:
- Attempts to create usernames
testuser01throughtestuser10with strong deterministic passwords. - Existing users are treated as non-fatal (
user_already_exists). - At end, script verifies each username can log in via
POST /api/auth/login. - Script exits non-zero if any signup/login operation fails unexpectedly.
Requirements:
- Dev server must be running at
http://localhost:3000. - Supabase URL/anon key configured in
.env.local. - Supabase Confirm Email disabled (username-only flow).
- Turnstile either disabled or configured with local/test keys.
Troubleshooting:
- If the script reports missing env vars, verify
.env.localand restartnpm run dev. - If the API returns
email_confirmation_enabled, disable Supabase Confirm Email. - If the API returns
profiles_missing, apply Supabase migrations withnpx supabase db pushbefore rerunning. - If logins fail unexpectedly, inspect server logs and Supabase Auth -> Users for account state.
Security note:
- Intended for local/dev only. Do not run against production datasets.
- Created auth users remain in Supabase Auth -> Users for manual inspection.
The repository includes a lightweight script to validate user-level favorites isolation in development.
Command:
npm run test:rls:devWhat it does:
- Creates/logs into two test users.
- Adds favorite tools for each user.
- Asserts each user only reads their own favorites from
/api/favorites. - Exits non-zero on any isolation failure.
Use this when changing auth/favorites policies or API logic.
- Set
.env.localwith Supabase + Turnstile keys (use always-pass test keys for Turnstile in local dev). - Run:
npm install npm run dev
- Seed and validate auth flow end-to-end:
npm run seed:test-users
- Verify signup/login:
/signupshould complete immediately (Confirm Email disabled)./loginshould authenticate with username + password.
- Verify usage limits:
- As guest, run a tool 5 times.
- The 6th run is blocked and shows login CTA.
- Verify favorites:
- Login, favorite a tool from the tool page action bar.
- Reload and confirm it remains favorited.
- Open another browser/device with same account and confirm sync.
- (Optional) Verify auth endpoint rate limiting after setting
AUTH_RATE_LIMIT_ENABLED=trueand Upstash env vars:- Signup: max 3 requests/hour/IP.
- Login: max 10 requests/15 minutes/IP.
- Verify RLS isolation:
npm run test:rls:dev
- Verify analytics:
- Confirm
/api/analyticsincrements only when runs succeed. - Confirm home page totals/per-tool counts increase after running tools.
- Confirm
Current production persistence model:
- Analytics: Supabase-backed (
public.tool_usage_totalsand optionalpublic.tool_usage_daily) via/api/analytics. - Reports: Supabase-backed (
public.reports) via/api/reportsand/api/admin/reports.
Important Vercel behavior:
- Vercel serverless/runtime filesystem is non-durable and may be read-only.
- Reports and analytics no longer depend on the local filesystem.
/api/reportsand/api/admin/reportsreturnreports_unconfiguredwhen Supabase admin configuration/migration is missing./api/reportsand/api/admin/reportscan returnreports_unavailableon transient Supabase failures.
Run this before creating or promoting a production deployment:
npm run lint
npm run test
npm run build:secure
npm run test:e2e- In Vercel, choose
Add New... -> Projectand import this Git repository. - Vercel auto-detects Next.js settings for this repo.
- Set Node.js version to
20.x(project baseline is Node.js 20+). - Trigger the first deployment.
Use the checklist in Environment Variables -> Vercel Environment Variables Checklist (copy/paste) above.
- Open
Project -> Settings -> Domains. - Add your production domain.
- For apex +
www, accept Vercel prompts to add both and choose your preferred redirect direction.
Prefer the exact DNS values shown in the Vercel Domains UI; do not assume static targets.
Recommended apex + www pattern:
- Add apex domain to the Vercel project and accept Vercel's
wwwsuggestion/redirect preference. - In Cloudflare DNS:
- Apex (
@): add anArecord pointing to Vercel (commonly76.76.21.21) or whatever Vercel currently shows. www: add aCNAMEpointing to the Vercel target (commonlycname.vercel-dns.comor a project-specificvercel-dns-###target).
- Apex (
- If Vercel shows an absolute CNAME value with a trailing dot (
.), copy it exactly, including the dot.
Subdomain-only pattern (for example tools.example.com):
- Add
tools.example.comin Vercel project domains. - In Cloudflare DNS, add
CNAMErecord:tools-> Vercel-provided target.
- SSL/TLS mode: use
Full (strict)where possible. If strict validation is temporarily blocked, useFullwhile resolving certificate issues. - Proxy (orange cloud) tradeoff:
- Start DNS-only (
Proxy status: DNS only) until Vercel verifies the domain and issues SSL certs. - After cert issuance, you can enable proxy if desired.
- If proxy is enabled and you hit certificate/redirect issues, temporarily switch back to DNS-only during troubleshooting.
- Start DNS-only (
Invalid configuration:
- Re-check Cloudflare records against the exact Vercel Domains UI values.
- Remove conflicting DNS records (especially stale/incorrect
AAAArecords; Vercel does not support IPv6 origin records for this setup). - Ensure only the intended apex/CNAME records exist.
Cert not issuing / failed to generate cert:
- Set the related DNS record(s) to DNS-only temporarily.
- Confirm CNAME target exactly matches Vercel (including trailing dot when shown).
- Remove and re-add the domain in Vercel Domains settings.
- Wait for DNS propagation before rechecking.
Redirect loops:
- Check middleware canonical host behavior (
CANONICAL_HOST) inmiddleware.ts. - Check Cloudflare
Always Use HTTPSand other redirect rules. - Avoid double-forcing redirects in both Cloudflare and app middleware with conflicting host/protocol targets.
Security header baseline:
- HSTS,
nosniff, referrer policy, permissions policy, and clickjacking protection are enabled globally. - CSP is enforced via
Content-Security-Policy(not report-only). - CSP uses a per-request nonce for inline scripts (
script-src 'nonce-<value>') so theme init and JSON-LD inline scripts remain allowed withoutunsafe-inline. script-srcdoes not useunsafe-inline; it allows'self', nonce scripts,blob:, Turnstile (https://challenges.cloudflare.com), and'wasm-unsafe-eval'for WASM-heavy browser tools.style-src 'unsafe-inline'is intentionally retained for App Router styling compatibility.- Turnstile sources are allowlisted in CSP (
https://challenges.cloudflare.com) for script/frame/connect.
CSP verification and troubleshooting:
- Open DevTools -> Network -> document request and inspect response headers.
- Confirm
Content-Security-Policyexists andscript-srcincludes'nonce-...'. - Confirm
script-srcdoes not include'unsafe-inline';unsafe-evalshould also be absent. - If a feature breaks, check the browser CSP console violation and add only the specific missing source/directive instead of broad unsafe fallbacks.
- Vercel env vars are set (see checklist above).
- Cloudflare SSL/TLS mode is
Full (strict). - DNS records point to Vercel using exact Vercel-provided targets.
- Canonical host config is aligned:
NEXT_PUBLIC_SITE_URLuses the intended canonical URL.CANONICAL_HOSTmatches canonical host when host redirects are desired.
Route and SEO checks:
https://<domain>/robots.txthttps://<domain>/sitemap.xml- Canonical tags match
NEXT_PUBLIC_SITE_URL. - Apex <->
wwwredirects behave as intended. /tools/<slug>redirects to/<slug>.- Run tools and confirm analytics totals/per-tool counters update on home.
DNS verification commands:
dig +short <domain> A
dig +short <domain> AAAA
dig +short www.<domain> CNAME
dig +short tools.<domain> CNAMENotes:
AAAAshould generally be empty for Vercel-hosted records in this setup.- DNS propagation can take time; allow for TTL before concluding a record is incorrect.
SEO sanity checklist:
- Run Lighthouse on
/and several high-traffic tool routes. - Run Google Rich Results Test on home and at least one tool page.
- Confirm generated metadata/OG tags reflect production canonical host.
/- Tool directory with search/filter and usage counter display./<slug>- Dynamic tool page (canonical)./tools/[slug]- Legacy route that permanently redirects to/<slug>./login- User login page (Turnstile-protected)./signup- User signup page (Turnstile-protected)./auth/callback- Supabase OAuth callback handler (Google scaffolding)./report- User report form./admin- Admin login + reports + analytics view./tester- Internal registry/routing sanity page.
All API responses use a JSON envelope:
- Success:
{ ok: true, ... } - Error:
{ ok: false, error: { code, message } }
Endpoints:
| Method | Path | Purpose |
|---|---|---|
POST |
/api/reports |
Validate payload and insert a report into public.reports (Supabase, server-side service-role client). |
GET |
/api/admin/reports |
Return up to 1000 newest reports from public.reports (admin cookie required). |
POST |
/api/admin/login |
Validate credentials and set signed ot_admin httpOnly cookie (24h TTL). |
POST |
/api/admin/logout |
Expire admin cookie. |
POST |
/api/auth/signup |
Verify Turnstile, create Supabase auth user, insert profile, and rollback partial failures when possible (optional rate-limiting scaffold can be enabled). |
POST |
/api/auth/login |
Verify Turnstile and sign in Supabase user session (optional rate-limiting scaffold can be enabled). |
POST |
/api/auth/logout |
End Supabase auth session. |
GET |
/api/auth/session |
Return current Supabase auth user (if logged in). |
GET |
/api/favorites |
Return logged-in user's favorite tool IDs. |
POST |
/api/favorites |
Add tool favorite for logged-in user. |
DELETE |
/api/favorites |
Remove tool favorite for logged-in user. |
POST |
/api/analytics |
Increment tool usage counter by slug (server-side Supabase write via service-role RPC). |
GET |
/api/analytics |
Return aggregate usage snapshot from Supabase analytics tables. |
Validation highlights:
- Reports: category must be
Bug,Feature,Security, orOther. - Reports: title max 120 chars, description max 4000 chars.
- Reports request size: 50 KB max (based on
content-length). - Reports storage errors:
reports_unconfigured(missing Supabase admin config/migration) andreports_unavailable(transient DB failure). - Analytics increment: slug must be non-empty and <= 80 chars.
- Auth: login/signup validates username + password and requires Turnstile token when enabled.
- Auth: username policy is lowercase
^[a-z0-9_]{3,20}$with reserved-name blocklist. - Auth: signup requires Supabase Confirm Email to be disabled for username-only flow.
- Auth: optional rate limiting returns
{ ok: false, error: { code: "rate_limited", message } }on limit breaches whenAUTH_RATE_LIMIT_ENABLED=true. - Favorites: tool IDs are validated against canonical registry slugs.
src/
app/
page.tsx
login/page.tsx
signup/page.tsx
auth/callback/route.ts
report/page.tsx
admin/page.tsx
tester/page.tsx
tools/[slug]/
api/auth/*
api/favorites/route.ts
components/
providers/
lib/
config.ts
config.server.ts
analytics.ts
usageLimiter.ts
supabase/
server/
adminAuth.ts
analyticsStore.ts
reportsStore.ts
http.ts
turnstile.ts
tools/
_shared/
registry.ts
loader.ts
<tool-slug>/
- Create
src/tools/<slug>/meta.ts,Tool.tsx, andindex.ts. - Export metadata + component from
index.ts. - Add the tool meta to
src/tools/registry.ts. - Add a loader case in
src/tools/loader.ts. - Enforce input limits in the tool UI before processing.
- Next.js app scaffolded with TypeScript + ESLint.
- Tool contract, registry, and lazy loader implemented.
- Home route lists tools from the registry.
- Dynamic route
/<slug>renders tools by slug. - User-safe error handling pattern is in place.
- Input/file limits are enforced per tool metadata.
- Security headers baseline (CSP enforced with script nonces/HSTS/nosniff/referrer policy).
- JSON Formatter/Minifier + validation.
- URL Encode/Decode.
- Base64 Encode/Decode.
- Case Converter.
- Image Resize + Compress with EXIF stripping.
- Global tool search.
- Category filtering (Text/Developer/Image/PDF).
- Per-tool SEO metadata.
- Privacy messaging for local processing.
- PDF Merge.
- PDF Split.
- PDF Rotate.
- PDF Extract.
- Strict PDF file/page limits.
- Clear local-processing messaging.
- Shared UI components (
Dropzone,FileList,OutputPanel,OptionsPanel,ErrorMessage,EmptyState). - Better empty/error states across tool pages.
- Basic accessibility improvements (labels, keyboard focus, visible focus styles).
- Lightweight analytics without tool-input logging.
- Report intake + admin dashboard for reports and analytics.
- Unit/integration tests for tool logic, registry/loader contract, and API validation.
- Playwright E2E coverage across all tools, including PDF/Image download flows.
- JSON Formatter + Validator.
- Base64 Encode/Decode.
- URL Encode/Decode.
- Hash Generator.
- UUID Generator.
- Case Converter.
- Timestamp Converter.
- CSV <-> JSON Converter.
- Whitespace Cleaner.
- Password Generator.
- YAML <-> JSON Converter.
- HTML/CSS Minifier.
- Regex Tester.
- JWT Decoder.
- PDF Merge/Split/Rotate/Extract.
- Image Resize + Compress.
- PDF Reorder tool.
- Standalone EXIF viewer/stripper tool.
- Production deployment playbook (Vercel + Cloudflare baseline) documented.
- Production security headers baseline verified.
- CI checks and dependency automation (Dependabot/audit pipeline).
- Retention policy and auto-delete for uploaded content.
- Malware scanning for uploads.
- Rate limiting and abuse prevention.
- Ephemeral object storage + queue/worker model.
- OCR and Office conversion tools.
- Tool content is processed in-browser, not uploaded by tool pages.
- Reports and analytics are intentionally minimal and avoid logging tool input payloads.
- Admin auth uses a signed cookie with timing-safe signature checks.
- API validation returns safe user-facing errors and does not expose stack traces.
- Vulnerability reporting process is documented in
SECURITY.md.
- This project is proprietary and distributed under an "All Rights Reserved" license (
LICENSE). - Client-side code can still be reverse engineered in compiled/minified form.
- Real protection for proprietary algorithms requires moving sensitive logic to server-side execution.
- A placeholder route exists at
/profor future server-side/premium tools, but no logic has been moved yet.
All Rights Reserved. See LICENSE.
Terms of use: TERMS.md.