Currently building: himasjid · Week 16
Perth, AU · GMT+8 00:00:00
Back to work
Project: Self-initiated · SaaS· Role: Solo builder· Timeframe: Apr 2026 — Present

Copypara.

An AI writing assistant that strips the tells from AI-generated text. Paste a draft, click humanise, get back the same ideas in a voice that reads like a person wrote it.

Status

Live · copypara.com ↗

Stack

Next.js 16 · React 19 · Tailwind v4 · Anthropic · Stripe · Prisma 7 · Postgres · NextAuth v5 · Upstash

Humanize History
19,720 / 20,000 words remaining

Thank you for reaching out to coordinate our schedule for tomorrow. I have reviewed the proposed timeline, and everything looks clear on my end, so I have officially blocked out the requested time in my calendar to ensure there are no overlapping commitments. I want to make sure we have the full window available to focus entirely on the agenda items without any interruptions or distractions, as I know how valuable this dedicated time is for keeping our momentum going.

I am particularly looking forward to diving into the specific project milestones we discussed previously, as this feels like the perfect moment to evaluate our current trajectory. It will be a great opportunity to align on the immediate next steps and address any outstanding technical questions or potential bottlenecks before we officially move into the next phase of development. If there are any additional documents, data sheets, or briefings I should review this evening to ensure I am fully prepared for the conversation, please feel free to send them over at your earliest convenience.

Thanks for reaching out about tomorrow. I've looked over the proposed timeline and it all works on my end, so I've blocked the time in my calendar. I want to keep that window clear so we can stay focused on the agenda without interruptions.

I'm especially looking forward to going through the project milestones we talked about. It feels like a good point to take stock of where things stand, agree on the next steps, and work through any open technical questions or potential bottlenecks before we move into the next phase. If there are documents, data sheets, or briefings I should read through tonight to come prepared, please send them over whenever you get a chance.

140 words
Humanize
131 words
§ 01 — Context

Every AI draft has tells. ChatGPT opens paragraphs with "In today's landscape". Claude reaches for em dashes — liberally — and lands every conclusion with "Ultimately." Gemini cycles through "Additionally" and "Furthermore" where a person would just keep writing. These aren't hallucinations; they're style signatures baked into training data, and once you recognise them they're impossible to unsee.

The problem isn't the content. The ideas are usually fine — well-structured, accurate, serviceable. It's the voice. For emails, blog posts, video scripts, anything meant to build a relationship with a reader, that register doesn't land.

I spent a few weeks trying to fix AI drafts by hand before noticing I was correcting the same patterns every time: the significance inflation, the false ranges ("anywhere from 10 to 30 percent"), the synonym cycling, the way every paragraph restates its opening sentence before it ends. Maybe fifteen patterns account for most of the tell, and they're consistent across models.

Copypara strips them. One system prompt, a flat list of rules, applied in a single pass and streamed token by token back to the browser.

Strip the tells. Keep the ideas.
§ 02 — Constraints

This is a product with real users and real money moving through it from day one. The constraints shape every decision differently than a side project that only I use.

Team size
One. Weeknights and weekends.
Budget
Free tiers where available. AI cost is the variable — every humanisation burns tokens and that cost has to sit under the subscription margin.
Pricing
Free: 2,000 words/month on Haiku. Pro: 20,000 words/month on Claude Sonnet 4.6 at $5/mo or $48/yr.
Quality bar
Auth, billing, and quota have to be correct — not "good enough for a side project". There's no tolerance for charging the wrong user or double-deducting words.
Scope
V1: Humaniser, Documents, Composer, Google OAuth, Stripe billing, rate limiting. V2: voice matching, tone slider, per-use-case presets.
§ 03 — Architecture

The core of the app is a streaming pipeline. A humanisation request checks and atomically deducts the user's word quota, opens an SSE connection to the browser, streams the Claude response token by token, then fires a separate non-streaming call to generate a title before writing the result to Postgres. If the user cancels mid-stream or the AI call fails, the word deduction is refunded in the same transaction. Nothing is lost and nothing is double-charged.

The Composer is the second surface — a persistent document editor where the user can select a span of text, submit a free-form rewrite command, and accept or reject the result before it's written back. It uses the same streaming infrastructure as the Humaniser but with a different system prompt and a plan-aware model router: Sonnet 4.6 for Pro users, Haiku 4.5 for Free.

Stripe sits outside the critical path but has to be handled correctly. The webhook loop syncs subscription state — plan, word limit, period end — into Postgres on every relevant event. The app reads plan from the database, not from Stripe directly, so a webhook replay or a brief Stripe outage doesn't affect the user experience. Monthly word quotas reset lazily on the first request after the billing period, not on a cron.

Upstash Redis handles rate limiting without a database round-trip on the hot path. Per-user keys with TTLs mean a user can't hammer the humanise endpoint even if they have remaining quota.

CLIENT Browser SSE STREAM · REACT 19 · ONE USER APP Next.js 16 · App Router SSE STREAMING · SERVER ACTIONS · WEBHOOKS DATA · AUTH · AI · BILLING · RATE LIMIT Postgres PRISMA 7 · NEON NextAuth v5 GOOGLE OAUTH Anthropic SONNET · HAIKU · SSE Stripe BILLING · HOOKS Upstash REDIS · RATE LIMIT LIME = CRITICAL PATH · SSE STREAM + PRISMA
Fig. 01 — System diagram. Streaming pipeline on the critical path; billing and rate-limiting off it.
§ 04 — Stack & reasoning
Next.js 16
App Router handles the static surfaces (landing, pricing) as server components and the authenticated app as a mix of server components and API routes. Streaming responses and Stripe webhooks need API routes — everything else is a server action.
React 19
The React Compiler is enabled via the Babel plugin, which means the component tree gets automatic memoisation without manual useMemo calls. Most of the app is server-rendered anyway; the React Compiler matters for the Composer editor where the component tree is deeper.
Tailwind CSS v4
No config file — design tokens live in @theme inside globals.css. The v4 approach keeps the styling co-located with the markup and means one fewer build-tool artifact to maintain.
Anthropic
Claude Sonnet 4.6 for Pro users, Haiku 4.5 for Free. The two-model split is the core of the freemium margin: Haiku costs roughly $0.04–$0.08 per Free user at the 2k cap; Sonnet costs $0.85–$1.00 per Pro user at the 20k cap. Both sit under the subscription price.
OpenAI
Secondary provider in the same abstraction layer. An environment variable switches the active provider without touching application code — useful as a fallback or for A/B testing output quality.
Prisma 7 · Postgres
Word tracking needs atomic deduct-and-refund semantics. Prisma's transaction API gives that cleanly. Neon's branching made schema iteration fast — dev branch isolated from production from day one.
NextAuth v5
Google OAuth only — one provider, one decision, nothing to rotate or maintain. The Prisma adapter handles the account and session models. No credentials flow; the attack surface for auth is minimal.
Stripe
Checkout for new subscriptions, Customer Portal for upgrades and cancellations, webhooks for subscription lifecycle events. Plan state lives in Postgres, not in Stripe, so the webhook just syncs a few columns and the app never blocks on a Stripe API call at request time.
Upstash Redis
Rate limiting without a database round-trip. Per-user TTL-based keys cap the humanise endpoint at the connection layer before quota logic runs. Upstash's serverless pricing means there's no persistent Redis instance to manage.
Vitest
Tests focused on the quota, deduction, and refund logic. The billing math — deduct, refund on failure, lazy monthly reset — cannot have an off-by-one error in production, so those paths have coverage. UI code does not.
§ 05 — Status

V1 is live at copypara.com ↗. The Humaniser with streaming, auto-save, and auto-generated document titles is shipped. Documents list, Google OAuth, Stripe billing with monthly word quotas, rate limiting, and the public landing page with pricing are all live. The Composer — persistent document editing with inline span rewrites and accept/reject — shipped the week after launch.

V2 is in progress: voice matching lets users upload a writing sample and rewrite in their own voice; a tone slider controls how aggressively the tells are removed; per-use-case presets (Email, Blog, Script) adjust the system prompt for context. The infrastructure is in place — the model routing, the document storage, the streaming pipeline. The V2 work is the interface and the system-prompt iteration.

§ 06 — Reflection

The hardest part wasn't the AI integration — it was the billing. Stripe's webhook model means the database and the payment processor are always eventually consistent, never immediately so, and the product has to work correctly in every intermediate state. A user who just upgraded should get Pro access before the page reloads; a user whose subscription lapsed should hit the Free word cap on the next humanisation; a user mid-stream when their monthly reset fires should have their quota handled correctly. Getting all of that right required thinking carefully about which state lives where and in what order it gets written.

The thing I'd do differently is treat the system prompt as data from day one. The humanisation rules are a flat list of patterns and bans — no em dashes, no "furthermore", no false ranges, no hedging closers. They already live in a file I treat like configuration, but they're still a string I interpolate rather than a structure I can version, diff, and test independently. That's V2 work, and it would make the prompt easier to iterate on without touching code.

Next case study

Caretbox

An open-source developer knowledge hub. Also the cleanest code I've written outside of work.