Recent decisions
Chronological decisions from 2026-04 onward that have not yet been filed under a topical group.
/api/v1 read endpoints use the existing web/lib/api-auth.ts helper, accept both API-key and JWT
Decision: New /api/v1 read endpoints (starting with /api/v1/workspace and /api/v1/workspace/members in #2421) consume the existing requireAuth() + requireScope() helpers in web/lib/api-auth.ts rather than inlining the ax_k_-only auth pattern the way /api/v1/content/route.ts does. They accept either an ax_k_ API key (with the required scope) or a Supabase JWT (which gets all scopes implicit via requireScope's JWT path).
Rationale: The #2421 body recommended extracting a new web/lib/api-v1-auth.ts helper -- that recommendation came from an incomplete repo scan during decomposition. The canonical helper already exists at web/lib/api-auth.ts and is what every /api/v2 route uses. Forking it for v1 would create a second copy of bearer-extract + key-hash-lookup + last_used_at update that would drift. The existing requireAuth() already supports both ax_k_ bearer and X-API-Key headers, falls back to JWT, and handles scope enforcement consistently. Reusing it makes v1 reads consistent with v2 and means later code (resources for sibling #2422, the openapi route for #2423) can compose against the same surface. Accepting JWT in addition to ax_k_ is not a security expansion -- a JWT caller is a logged-in session user who can already read their own workspace through the app UI; the endpoint just gives them a programmatic path to the same data.
Launch pricing: Free $0 with 3-piece cap, Pro at $99, Team deferred from public pricing
Decision: Public pricing becomes a two-tier shape: Free at $0/mo with a 3-pieces-per-month cap (LinkedIn + Markdown export only) and Pro at $99/mo (unlimited pieces, every platform, every export, newsletter intros, eclectis integration, custom workspace branding). The legacy Solo tier is replaced by Pro at the same $99 price point so existing Solo subscribers carry over without a billing-amount change. The legacy Team tier ($249/mo, multi-workspace) is removed from the public pricing page but remains active for existing Team subscribers and admin-assigned workspaces -- no forced migration, no archival in Stripe.
Rationale: Three load-bearing arguments, per the pitch on #2228 (decomposed) and the decomp note dated 2026-05-17 in this file.
(1) Phase-1 recruitment math. "Find 3 case-study users willing to pay $99 before they've tried it" is a slow, narrow funnel that selects the wrong people -- the willing-to-pay-before-using cohort is not the same cohort as the users-who'd-most-benefit cohort. "Let anyone try it, the ones who love it self-identify" is a faster, wider funnel that produces both behavioral signal (patterns surface at 50+ users that don't surface from a single paying user feedback loop) and warmer conversion candidates (people who already chose the product).
(2) Category parity. Bloomberry, Oiti, EasyGen, and Meet Sona -- the named competitors in our category -- are all freemium. A paid-only entry point reads as friction inside the category, not as positioning above it. The "we charge so we're serious" frame doesn't earn its keep when every comparison search ends at our competitor's free tier.
(3) Throttle infrastructure was already shipping. Sub-issues #2229 and the #2356--#2362 chain landed the plan-field extension to free/pro, the monthly piece counter, the tier-gate that returns 402 on Free overrun, model-routing for cost control on free traffic, and export gating. Pricing copy was the last public-surface piece -- the engineering side that makes a capped Free tier safe to operate had already shipped.
Pro at $99 (unchanged from Solo) and Free at 3 pieces/month are the ship defaults. If Paul shifts either digit on #2228, each public surface (this file, web/app/pricing/page.tsx, the Stripe product description in #2390, the landing CTA in #2391) is a one-string change.
daily.scan_and_generate_ideas sources from eclectis when configured; Serper kept as fallback
Decision: The daily.scan_and_generate_ideas handler (#2402) now calls get_eclectis_client_for_workspace and, if a client is returned, sources idea-generation signal from list_articles(since=now-7d, limit=20) instead of running Serper Google searches. The Serper path (search_terms -> search_google -> dedup -> cap) lives unchanged inside the else branch and remains the path for workspaces without an eclectis integration. The Serper API key check moved inside the else branch -- eclectis-only workspaces no longer error out when SERPER_API_KEY is unset. As with #2403, when eclectis IS the configured source and the call raises, the handler does NOT silently fall back to Serper.
Rationale: Same logic as the curated_brief cutover (see prior entry): a workspace that has cut over to eclectis is by definition not running the legacy article-scan handlers, so falling back to a stale local source on eclectis failure would mask outages. Keeping Serper inside the else branch means not-yet-cutover workspaces keep their existing path with zero behavior change. A small _eclectis_article_to_search_result mapper adapts EclectisArticle to the search-result dict shape (source_name -> source_domain, summary || snippet -> snippet) so the existing prompt assembly, Claude call, and idea-insert logic stay unchanged -- only the source-of-articles seam swaps.
curated_brief.generate sources from eclectis when configured; no fallback to local on eclectis failure
Decision: The curated_brief.generate handler (#2403) now calls get_eclectis_client_for_workspace and, if a client is returned, sources articles from eclectis via list_articles(since, limit=500) instead of the local articles table. If the workspace has no active eclectis integration, the handler falls through to the existing local-articles SQL query unchanged. When eclectis IS the configured source and the call raises (auth, network, server error), the handler does NOT fall back to the local query -- the EclectisError propagates and the command goes to dead-letter.
Rationale: A cutover workspace's local articles table is by definition stale relative to eclectis -- that table only gets writes from the legacy scan handlers, which the workspace has presumably stopped running. Silently falling back to a stale local store would produce a half-correct brief that masks the outage and ships under the workspace owner's name; dead-letter + visible failure is the right shape ("default silent; earn the send"). The local path stays as a fall-through for not-yet-cutover workspaces (no eclectis integration row) so existing pre-cutover workspaces keep working. A small _eclectis_article_to_row mapper at the boundary swaps source_name -> publication and score -> ai_score so _group_articles and curated_brief_template stay unchanged. Parallel handler daily_scan_and_generate (#2402) is the next cutover.
Landing hero adopts Option B ("Your ideas, in your voice, published consistently. Without the grind.") for the Phase 1 GTM
Decision: The Phase 1 GTM landing reshape (#2338) ships with the Option B hero from the spec at docs/superpowers/specs/2026-05-11-landing-solo-thought-leader-first.md -- headline "Your ideas, in your voice, published consistently. / Without the grind." with a sub-head that names founders, consultants, and executives directly and positions Authexis as "the production team." The Option A candidate ("Everyone gets their ideas out. / Judge the outcome, not the origin.") was not taken.
Rationale: Option B is a restructuring of the PRODUCT.md tagline shorthand already locked in SEO.md ("Publish consistently, in your voice, without the grind."). It leads with concrete outcome and names the audience by title, which the spec calls out as the Phase 1 GTM intent (solo thought leaders as the first example, not the only audience). Option A is principle-led and answers the AI-legitimacy objection, but for a landing page that has no social proof yet, outcome-naming + audience-naming converts better than manifesto. The spec explicitly left the call to Paul; Paul declined to pick in the implementing session, so the call was made against the GTM brief rather than blocked. If Paul wants to revisit, the swap is a one-string change in web/app/page.tsx.
One-piece-to-many free tool mirrors the prior free-tool chain shape; trial import lands at approval-pending; newsletter is skipped at import
Decision: When decomposing the one-piece-to-many free tool (#2261), split the work into the same 3-child shape used for voice-analyzer (#2268/#2269/#2270) and calendar-generator (#2271/#2272/#2273): (a) adaptation service + minimal form, (b) polished result UI + trial-handoff CTA, (c) trial-pre-fill consumer that imports the source piece as a contents row plus each platform adaptation as a social_posts row. The five supported platforms are LinkedIn (≤3000), Threads (≤500), Bluesky (≤300), Mastodon (≤500), and a newsletter intro (80--220 words). The newsletter intro is not inserted into social_posts at import -- it lives on the imported contents.intro column -- because social_posts is a social-platform table and newsletter isn't in its platform CHECK. Imported social posts land at social_posts.status = 'draft' with scheduled_at = NULL (approval-pending state from parent comment Option 2). Web-side direct Anthropic call (no engine queue) and durable rate-limit via checkToolRateLimit are inherited from the prior chain -- not re-litigated.
Rationale: The free-tool chain shape is a project pattern now (two completed chains, identical decomposition). Re-deriving boundaries for the third tool would burn coordination cost without product value. Paul's 2026-04-29 clarification on the parent settles the trial-handoff state question -- approval-pending preserves the existing queue-with-review primitive and matches Principle 7 (the team did the production work; the user makes the editorial call). Skipping the newsletter at import is schema-correct rather than schema-distorting: overloading social_posts with a newsletter platform would mis-classify the table; a separate newsletter_drafts table is the right future shape if newsletter pre-fill becomes a real need. Per-platform character caps are enforced in the prompt and validated post-extraction so the tool can't silently ship "fast generic" output -- the exact failure mode the parent positions against. Spec at docs/superpowers/specs/2026-05-17-one-piece-to-many-free-tool.md.
Free/Pro launch is decomposed as marketing + docs + Stripe + landing-CTA; signup-default and Stripe Team archival deferred
Decision: When decomposing the Free/Pro pricing-launch pitch (#2228), split the non-infrastructure work into four task-do children: pricing-page rewrite (#2388), PRODUCT.md + DECISIONS.md commercial entry (#2389), Stripe product/price alignment (#2390), and landing-page "Start free" CTA (#2391). Engineering plumbing -- plan field, monthly quota, server gates, exports, model routing, data migration -- was already covered under GH-2229 (#2356--#2362). Ship defaults: Pro = $99 (unchanged from Solo), Free cap = 3 pieces/month (matches #2229 infra default). Two adjacent paths are explicitly NOT in this decomposition: (a) flipping the new-workspace default from trial to free (still trial -> free fallback per existing migration default), and (b) archiving the Stripe Team product (Team stays alive in code and Stripe for existing subscribers and admin-assigned workspaces; only hidden from the public pricing page).
Rationale: The infra side already shipped the gates that make capped-free safe -- what's left is the public surface and the rationale-of-record. Splitting the four surfaces (pricing UI, internal docs, Stripe receipts, landing CTA) by file/system gives each child a single owner and a clean rollback. The signup-default change is a separate product call that touches trial mechanics, 14-day windows, and billing flow -- load-bearing on its own and not required for the Phase-1 recruitment thesis the pitch is chasing. Team archival is similarly out of scope: existing Team customers don't need disruption, and the public pricing page can simply not render Team without any code-side removal. Defaults pre-baked at $99/3 so each child has shippable concrete numbers; if Paul shifts either digit on #2228, a single string change per surface covers it.
Voice-interview-by-phone reuses the existing transcription handler; state lives in a voice_calls table; codes live per-content
Decision: When decomposing the voice-interview-by-phone feature (#2177), the inbound Twilio Voice webhook is a single stateful endpoint reading state from a new voice_calls table keyed by CallSid. Audio is pulled from Twilio and re-uploaded to the existing voice-recordings Supabase bucket; the existing content.transcribe_recording handler runs unchanged. The 6-digit content code is a new contents.voice_code column (globally unique, only callable while content is in interview status). The webhook validates with the existing verifyTwilioSignature helper. No new Vercel env vars; TWILIO_AUTH_TOKEN already present.
Rationale: The iOS voice-recording pipeline (voice_recordings table -> content.transcribe_recording -> Whisper -> interview_qa) is already in production. Reshaping the phone path to produce identical voice_recordings rows is cheaper and safer than building a parallel handler. A stateful endpoint with a DB-backed state machine is the right shape for an IVR with re-record and skip transitions -- bouncing state through TwiML action= URLs has no audit trail and breaks on * (re-record). Per-content codes (not per-workspace) match the granularity -- a caller dials in to answer questions for a specific piece -- and the status='interview' gate makes brute force non-viable.
Slug-keyed default prompts replace content_type_id prompt rows
Decision: Content-type-specific prompt defaults for the restored workspace_content_type_configs lane use stable default_prompts.key values such as speaking_notes:draft and speaking_notes:outline. Per-workspace overrides still live on workspace_content_type_configs.prompt_override_*; slug-keyed defaults sit below those overrides and above generic stage/field prompts. Python prompt constants remain as final fallbacks when the seed row is missing.
Rationale: The old content_type_id prompt tier was intentionally removed with the March 2026 content pipeline simplification. The speaking-engagement lane needs meaningfully different prompts per slug without reviving the dropped content_types table or overloading the generic piece.content default for every content type.
Multi-client content-management lead magnet uses polymathic-h source plus Authexis Team mirror
Decision: When decomposing the "How to manage content for multiple clients" lead magnet (#2265), treat the canonical article as polymathic-h content and the Authexis page as the conversion mirror. The source post lives in ~/Projects/polymathic-h/content/posts/how-to-manage-content-for-multiple-clients.md; Authexis mirrors it at /resources/how-to-manage-content-for-multiple-clients with syndicatedFromUrl pointing back to polymathic-h and a primary CTA to the Authexis Team trial. The issue's supplied H1, "Keep every client in their own voice," should be preserved as the reframe but wrapped in a keyword-compliant title such as "How to manage content for multiple clients without flattening their voices."
Rationale: This query has the strongest multi-client consultant intent in the current lead-magnet set. The article should demonstrate the operating system Authexis sells to marketing consultants: one workspace per client, separate voice profiles, batchable stages, approval buffers, and scheduled queues. The decomposition keeps canonical editorial work in polymathic-h, uses /resources as the product funnel, and avoids reopening the separate /for-consultants pillar or one-piece generator work.
Generic-AI-writing lead magnet uses polymathic-h source plus Authexis resources mirror
Decision: When decomposing the "Why does AI writing sound generic" lead magnet (#2264), treat the canonical article as polymathic-h content and the Authexis page as the conversion mirror. The source post lives in ~/Projects/polymathic-h/content/posts/why-does-ai-writing-sound-generic.md; Authexis mirrors it at /resources/why-does-ai-writing-sound-generic with syndicatedFromUrl pointing back to polymathic-h and a primary CTA to /tools/voice-analyzer before Solo trial signup. The issue's supplied H1, "AI sounds generic when it has nothing to work with," should be preserved as the reframe but wrapped in a keyword-compliant title such as "Why does AI writing sound generic? It has nothing to work with."
Rationale: This is the sister piece to #2263 and follows the same canonical/mirror split. The exact query has to appear in load-bearing SEO positions, but the article's strategic claim is broader than keyword placement: generic output is a missing-input problem, solved by perspective, examples, constraints, interview capture, and review rather than vague "be more human" prompting.
Voice-training lead magnet uses polymathic-h source plus Authexis resources mirror
Decision: When decomposing the "How to train AI to write in your voice" lead magnet (#2263), treat the canonical article as polymathic-h content and the Authexis page as the conversion mirror. The source post lives in ~/Projects/polymathic-h/content/posts/how-to-train-ai-to-write-in-your-voice.md; Authexis mirrors it at /resources/how-to-train-ai-to-write-in-your-voice with syndicatedFromUrl pointing back to polymathic-h and a primary CTA to /tools/voice-analyzer before Solo trial signup. No new voice-analyzer service work or RSS-to-resources architecture is part of this decomposition.
Rationale: The issue was filed from the Authexis lead-gen sheet, but the 2026-04-28 decision says long-form posts are authored in polymathic-h and product sites are syndication/conversion endpoints. /resources remains the cluster-based lead-magnet hub, while /blog already consumes the Authexis-tagged polymathic-h feed. A canonical-backed mirror preserves the product funnel and avoids re-opening the global /blog versus /resources strategy.
Public API contract lives in /api/v1; MCP wraps REST
Decision: When decomposing the public REST API + MCP server feature (#2230), make /api/v1 the stable public contract and keep /api/v2 as internal/mobile-ish. Existing /api/v2 behavior can be reused by extracting shared helpers, but external developers and MCP clients should target documented /api/v1 endpoints. The Authexis MCP server should wrap the public REST API for behavior REST owns, instead of duplicating database writes through engine/engine/services/assistant_tools.py.
Rationale: The repo already has broad /api/v2 routes, narrow /api/v1 routes, API keys, export routes, and an MCP server. Treating the broad internal surface as public would ossify internal response shapes and scope gaps. Keeping REST as the mutation source of truth gives developers a normal API while letting MCP remain an adapter for AI clients. This also prevents content, idea, publishing, and export behavior from forking across Next.js routes and Python MCP tools.
Universal-surface thesis: capabilities advance, positioning holds
Decision: When decomposing the "universal content creation surface" thesis (#2222), build the product capabilities (eclectis as canonical article source, eclectis article as interview source, post-approve distribution nudge, MCP client) while explicitly NOT rewriting landing copy or PRODUCT.md positioning. The thesis described both -- a product capability expansion AND a positioning rewrite from "thought-leader ghostwriter replacement" to "any input, any output." We split them.
Rationale: The day before this decomposition (2026-05-11), Paul shipped #2220 -- landing reshape to "solo-thought-leader-first," explicitly narrower positioning. That decision is fresh and load-bearing. The product CAN serve any content while marketing stays narrow on the wedge that closes. Reopening positioning here would re-litigate a 24-hour-old call. Capability work advances independently. If/when the broader positioning becomes the right wedge, it's a separate strategic call, not a side effect of a build cycle.
Free tier is capped access, not read-only or unlimited
Decision: When decomposing the free-tier launch infrastructure (#2229), treat free as capped access: free workspaces can create a small number of content pieces each month and use LinkedIn/Markdown basics, while Pro gates unlock unlimited monthly content creation, multi-platform distribution, newsletter generation, PDF/DOCX export, Eclectis integration, and custom branding. Keep active trials full-access for now. Preserve admin override so Paul/Ax can set workspaces to pro for testing and grandfathering.
Rationale: The codebase already has trial | free | pro | team plan values and a narrow plan-gates helper, but current free behavior is read-only. A real free launch needs product value without unbounded compute cost. Capped creation gives users a meaningful sample while the server-side quota and engine cost controls protect margins. This is infrastructure only; pricing page and PRODUCT.md copy changes remain separate launch work.
Phase 2 output modes prioritize branded docs and Google Slides partnership
Decision: Phase 2 output-mode work (#2226) should extend the existing final-content export routes instead of replacing them. Build telemetry and workspace export-branding controls first, then branded PDF via HTML/CSS tokens, then branded DOCX via Word style definitions. For slide decks, integrate with Google Slides and return an editable deck URL; do not build a native themed slide/design engine. EPUB is deferred until long-form institutional demand is visible.
Rationale: Phase 1 already shipped plain MD/PDF/DOCX exports. Branded PDF is the highest-confidence path because HTML remains canonical and CSS can carry brand tokens. DOCX brand fidelity is useful but lower-confidence because Word's style/template model is finicky. Slides-with-theme is a design-engine problem with strong incumbents, so partnership is the defensible path. The work should be usage-gated so Authexis does not over-invest before export behavior proves real demand.
Data & privacy
User profile data stays in platform
Decision: Profile data stored only in Authexis database. Never synced to third-party services, LLMs, or external platforms. AI prompts reference profile data but don't send it to model training. Users can export their data but we don't push it anywhere.
Article research data is transparent
Decision: Show users which articles were found, assessed, and scraped. Display relevance scores in admin UI. Never hide research failures -- show "scrape failed, used snippet instead."
User experience
Inbound replies prefer tagged workspace context over membership guessing
Decision: Resolve inbound email/SMS replies from explicit tagged context when the reply address encodes a known record. If a sender has more than one active membership and no trusted workspace context, reject the inbound reply instead of silently choosing one workspace.
Rationale: Protects cross-workspace data integrity. Tagged reply addresses carry the safest available routing signal.
Social slot presets stay in existing scheduling model
Decision: Keep cadence presets as a one-time seeding/reset layer on top of posting_slots. No new database tables or persistent "preset" state. Generate slots only for connected social providers.
Maya-only email workflow
Decision: Maya is the only character email in the system. Stage approvals and notifications come from Maya. Web UI remains available but optional.
Features & scope
In-app voice recording for interviews
Decision: Build voice recording into the interview stage. Users tap to speak, speech is transcribed and becomes the interview answer. Web uses Web Audio API, iOS uses native recording. Raw audio discarded after transcription -- only text is kept.
Rationale: The record-in-another-app -> transcribe -> paste workflow is the #1 friction point in the interview flow. ASR is now commodity (Whisper, Cohere Transcribe). Voice recordings also capture authentic speaking patterns -- richer signal for voice matching than typed answers. See #2020.
Source material intake: three paths, same pipeline exit
Decision: Source material intake (paste transcript, paste URL, upload recording) replaces the user-answers-questions step at the interview stage. All three paths write to interview_qa and set status to interview -- identical to what a live interview produces. The pipeline from interview onward (draft generation, review, final) is unchanged. No new tables; one new column (source_material_text on contents) for raw provenance. The voice_recordings table and Whisper pipeline are reused as-is for the upload recording path. See #2160.
Rationale: Reuse over reinvention. The existing content.parse_transcript and content.transcribe_recording handlers cover two of the three paths without modification. Only the URL path needs a new handler (content.extract_source_material), which wraps the already-existing extract_from_url service. Three separate UI issues (not one) because upload recording has a meaningfully different two-phase async flow.
No team collaboration features
Decision: Content belongs to one user (creator). No comments, no approval workflows, no multi-author editing. Workspace-level settings for brand consistency.
Rationale: One voice = one person. Target users are solo consultants, not content teams.
Personal access tokens for browser extension
Decision: Support email/password and personal access tokens. Token login preferred (simpler, more secure, easier to revoke). 90-day lifetime. GET /api/auth/me validates token and returns user + workspaces.
No WordPress plugin for v1
Decision: Provide markdown export with frontmatter. Users copy/paste or use their existing workflow. Revisit if user demand is high.
Architecture
Monorepo structure
Decision: Single Git repo: web/ (Next.js), engine/ (Python), apple/ (iOS/iPadOS/macOS), browser-extension/, docs/. Deploy each independently (Vercel for web, Railway for engine, App Store for iOS).
Use production database in development
Decision: Development connects to production Supabase database. No local database, no seeds. CRITICAL: any DB queries affect production.
Rationale: Eliminates schema drift. Supabase connection pooling handles dev + prod traffic.
AI provider: Claude Sonnet via adapter pattern
Decision: Claude Sonnet is the default model. Adapter pattern abstracts provider choice. No user-facing model selection UI.
Rationale: Users want results, not configuration. We can switch providers without user-facing changes.
Apple app: native Swift/SwiftUI, universal across iOS/iPadOS/macOS
Decision: Native Apple app (Swift + SwiftUI), universal across iOS/iPadOS/macOS in apple/. No React Native, no Flutter. Direct API calls to backend. Apple-specific features: voice recording, push notifications, widgets, Share Extension. No Android planned for 2026.
Rationale: Target audience skews heavily Apple-platform. SwiftUI lets a single codebase serve iPhone, iPad, and Mac with platform-appropriate idioms. Legacy ios/ directory inlined to apple/ Feb 2026 -- read-only reference.
Social publishing: LinkedIn, Bluesky, Mastodon via OAuth
Decision: Support LinkedIn, Bluesky, Mastodon via OAuth. No Twitter/X (API pricing too expensive). No Facebook/Instagram (wrong audience).
Authexis ↔ Eclectis: support both REST and MCP
Decision: Eclectis exposes both surfaces; authexis consumes both. Server-to-server fleet traffic (background crons, sidebar fetches, idea-generation in the engine) uses REST (GET /api/v1/articles etc., per-user bearer token). User-facing AI integrations (Claude Desktop, Cursor, ChatGPT desktop) consume eclectis's MCP server. Both are first-class.
Rationale: Different audiences need different protocols. REST is right for program-to-program -- stateless, known endpoints, low overhead, no session/tool-discovery dance. MCP is right for AI sessions -- tools and resources are discoverable, the AI can use them flexibly without per-endpoint client code. Initial 2026-04-24 framing of "REST not MCP" was too sharp; Paul's 2026-04-25 correction: "we support BOTH." Authexis already does -- engine/MCP server (engine.authexis.app/mcp/) + the REST client at web/lib/eclectis-client.ts.
No tier-1 byline / book / academic submission lane
Decision: Authexis will not build product surfaces that help users submit AI-assisted writing to tier-1 publications (HBR, WSJ, Forbes, Fast Company, Wired), book agents/publishers, or academic journals. This includes "scaffolding" or "research-only" variants -- the distinction between AI-helped and AI-ghostwrote is not legible to real audiences in these domains, and the stigma is binary regardless of intent.
Rationale: Paul's 2026-04-24 observation: "NO ONE understands 'AI helped' vs 'AI 100% wrote this without me'. And now we're into The Scarlet Letter. It's real." Risk asymmetry is unfavorable -- one AI-detection event at a tier-1 outlet is a permanent burn on the author's reputation at that outlet, which spreads. Product usage has to be defensible by the user publicly; these domains don't offer that. LinkedIn posts, newsletters, social content, internal documents, and tier-2 publications are fine -- the trust norm there is different.
Superseded: #2233 (rescoped and then closed); part of #2234's scope (keynote abstracts stay in, but not the speaking-related academic-paper variant if one were proposed).
AI-assisted is product truth; landing page leads with outcome
Decision: "AI-assisted" stays in product copy, marketing collateral, and SEO targeting -- we don't lie about what the tool does. SEO keyword set keeps both outcome-framed terms ("ghostwriter alternatives", "consistent content publishing") and AI-framed terms ("AI writing tools for thought leadership", "AI-assisted vs AI-generated"). What changes is the landing-page register: lead with the outcome ("publish consistently in your voice"), describe the mechanism (AI-assisted) further down, never bury or deny it.
Rationale: Paired with the Scarlet Letter decision above, but a different axis. Scarlet Letter says: don't build product surfaces that pretend AI didn't help. This says: don't pretend AI didn't help in marketing either. The two together mean the user's experience is outcome-first ("I published this") and the product description is honest ("AI-assisted, in your voice") -- and we capture SEO traffic from people who already know what they're looking for ("AI writing tools") because honesty wins them when they land.
Content pipeline simplification (March 2026)
These decisions collectively replaced the old multi-stage, multi-content-type system.
Hide research layer -- Eclectis owns content reading
Decision: Hide feeds, articles, sources, scans, bookmarks from Authexis UI. Google search scanning continues as invisible background plumbing fueling idea generation. Eclectis handles content reading and curation.
Eliminate content types -- length is the only variable
Decision: Remove the content type system entirely. Content creation asks for: idea, target length, and optional steering guidance. One pipeline for everything.
Simplify stages to 4 user-facing states
Decision: Users see: generating → interview → review → final. Internal stages (outline, script) happen behind the curtain during "generating."
Flatten stage_data into proper columns
Decision: All content fields moved from stage_data JSONB to dedicated columns on contents. Real columns are queryable, indexable, and work with Supabase realtime.
Replace stage/stage_status with single status column
Decision: Single status column with 4 values: generating, interview, review, final.
Flow: create → generating (outline+script) → interview (user answers) → generating (draft+intro+image) → review (user approves/redoes) → final
Skip articles table -- generate ideas directly from search results
Decision: Google search results passed directly to idea generator as ephemeral context. No articles stored, scored, or fetched.
Simplify briefings to deterministic data assembly
Decision: Briefings are pure data assembly -- new ideas + content in progress. No Claude API call needed.
Post-process smart quotes instead of relying on AI
Decision: All generated text post-processed for smart quotes, apostrophes, and em dashes. Deterministic, not dependent on model compliance.
Drop content_types table, content_type_id and stage_data columns
Decision: Dropped content_types and workspace_content_types tables. Removed content_type_id from contents, default_prompts, workspace_prompts, custom_prompts. Removed stage_data JSONB. Prompt cascade simplified to workspace → default (no content type layer). Irreversible migration.
Stage check constraint includes all active content stages
Decision: contents_stage_check allows 13 values to accommodate legacy content: idea, outline, script, interview, piece, review, final, reaction, post, newsletter, bibliography, draft, article.
Auth completion events use signed_in
Decision: Auth server actions redirect on success, so completion is inferred from the signed_in event in sign-in-tracker.tsx. Auth tracking fires attempted and failed from the client.
Business
Pricing: Solo $99/mo, Team $249/mo
Decision: Solo plan $99/month (1 workspace). Team plan $249/month (multiple workspaces). Anchored against copywriter cost ($1,500-2,000/month), not against other SaaS tools.
Partner program: 35-40% recurring commission
Decision: Affiliate Partners: 35% recurring on Team plan. Premium Partners: 40% (10+ active referrals). Lifetime commission. Solo plan excluded.
Single attribution, no split commissions
Decision: One partner per referral gets full commission. First-touch attribution.
Outreach infrastructure belongs in paulos, not authexis
Decision: Authexis generates email text when asked. Everything after that (draft creation, sending, monitoring, prospect management, drip sequences) is paulos's responsibility.
Cold outbound email 4: outline, not full article
Decision: Email 4 delivers a structured outline in the prospect's voice instead of a full article. Demonstrates intelligence without risk of mediocre generated piece.
Account deletion: data first, auth last
Decision: Delete workspace data before auth user. If data cleanup fails, return 500 so the user (still logged in) can retry. Auth deletion happens last as the point of no return.
Rationale: The reverse order (auth first, then data) leaves orphaned data with no retry path -- the user is already logged out. Data-first means the only unrecoverable state is "data gone + auth delete failed," which is logged and recoverable by support.
WordPress blog publishing: draft via REST API, no term/media management
Decision: WordPress blog posts publish as drafts via /wp-json/wp/v2/posts with markdown→HTML conversion. Categories, tags, and featured images are not sent to WP in v1 -- users assign them in WP's admin after reviewing the draft. This avoids the complexity of WP term ID resolution and multipart media upload.
Rationale: WP's REST API requires numeric term IDs (not names) for categories/tags, which would require create-or-find API calls per term. Featured images require multipart upload to /wp/v2/media followed by a second update to set featured_media. Both are deferrable -- the draft workflow means users review in WP anyway.
Blogs are authored in polymathic-h and syndicated to product surfaces
Decision: Long-form blog content (/resources/* on authexis, equivalent surfaces on eclectis and other product sites) is authored in ~/Projects/polymathic-h as the canonical home. Product surfaces are syndication endpoints, not authoring endpoints -- they pull or mirror from polymathic-h with rel="canonical" pointing back. Product-specific CTAs (free-tool funnels, trial signups) are injected at syndication time, not baked into the source post.
Rationale: Centralizes long-form writing under one editorial surface so the same essay doesn't get re-edited, voice-drifted, or fork-rotted across N product sites. Keeps SEO credit consolidated on polymathic-h and avoids duplicate-content penalties via canonical links. Lets product sites stay code surfaces with the marketing layer thin and replaceable.
Implementation: Open per #2266 -- Hugo-native per-tag RSS feed (/tags/authexis/index.xml) is the chosen mechanic. #2254 (content-ideas-for-consultants) shipped as authexis-original before this rule landed; per Paul 2026-04-28, migrate it back to polymathic-h.
No dates in blog slugs
Decision: Blog post URLs do not contain publication dates. The slug is the topic-keyword slug only -- /resources/content-ideas-for-consultants, not /resources/2026-04-26-content-ideas-for-consultants. Applies to authexis /resources, polymathic-h posts that syndicate here, and any future product surface that mirrors from polymathic-h.
Rationale: Date-stamped slugs make a post look stale the moment the year ticks, even when the content is still accurate; they also lock the URL to a publication moment that often doesn't match when the content was last meaningfully revised. Topic-only slugs let evergreen content stay credible without rewriting URLs to keep them current. Date is metadata (rendered as <time> on the page, sortable in lists, present in pubDate for RSS) -- it doesn't need to be in the URL.
Implementation: When the syndication mechanic in #2266 fetches polymathic-h posts that have a Hugo-default date-prefixed permalink, strip the date prefix when rendering at /resources/. Source-side: polymathic-h's permalink config should already produce date-free URLs for posts intended to syndicate (Charlie's lane on the polymathic-h side).
Blog post lead magnets must use their target SEO keyword in title and body
Decision: Every blog post written as a lead magnet (the cluster posts in /resources/) must implement its target SEO keyword as a real string in the title, the URL slug, the first paragraph, the meta description, and naturally throughout the body. Semantic variants alone are not sufficient -- the exact keyword phrase from the lead-gen sheet's Target Search Query column has to appear, because that's the query the post is trying to rank for. Captured-but-unused keywords (target keyword in frontmatter only, body uses related variants) is the failure mode this rule prevents.
Rationale: A lead-magnet post that doesn't contain its target query in load-bearing positions can't rank for that query. Search engines weight exact-phrase matches in title, H1, URL, and body for keyword relevance. Voice and humanize discipline still apply -- keyword stuffing is a separate failure -- but the choice is between natural inclusion and natural exclusion, not between good writing and SEO. The phrase has to be present and has to read.
Implementation: Author-side checklist: keyword in title (or close paraphrase that contains the exact phrase), in slug (already enforced by topic-only-slug rule), in meta description, in first paragraph of body, in at least one H2, and naturally repeated 2-4x through the body. Apply also to targetKeyword frontmatter so it stays auditable. /humanize pass remains required after the keyword pass -- keyword inclusion never overrides voice.
Predecessor: #2254 shipped before this rule landed with the keyword only in slug + frontmatter, missing from title and body. Fixed in the same commit that captures this rule.
CI / testing
CI bypasses Turnstile via env-driven sitekey, not a /dev/login backdoor
Decision: Turnstile sitekey is read from NEXT_PUBLIC_TURNSTILE_SITE_KEY (with the prod sitekey as fallback for any environment that hasn't set it). CI sets that var to Cloudflare's documented always-pass test sitekey (1x00000000000000000000AA) and the matching TURNSTILE_SECRET_KEY (1x0000000000000000000000000000000AA). Tests go through the real login form and the real signIn server action. Production code paths are unchanged.
Rationale: The alternative was extending /dev/login to work outside NODE_ENV === 'development' (gated by an ALLOW_DEV_LOGIN flag). That would have introduced a permanent auth-bypass surface in the production codebase -- anyone with the env flag set and admin Supabase credentials could mint sessions. Path B costs slightly more code but no production code path is structurally altered, and Cloudflare's test sitekeys are intentionally public so committing them to workflow YAML is not a secret leak.
Implementation: Site key reads from env in web/app/(auth)/login/page.tsx and web/app/(auth)/signup/page.tsx. Server-side web/actions/auth.ts:verifyTurnstile was already env-driven on the secret. Both .github/workflows/e2e.yml and .github/workflows/smoke-daily.yml set the test pair as plain env (not secrets -- they are public). web/e2e/auth.setup.ts waits for the submit button to be enabled (Turnstile widget produces token) before clicking.
Discovered via: #2284 -- auth.setup.ts had been failing in CI since 2026-04-17, the day Turnstile gating was added to the login submit button. Every authenticated Playwright test silently broken until smoke-daily (#2283) made the pattern visible.
Decision template
### [Decision title]
**Date:**
**Decision:**
**Rationale:**
**Tags:**
When in doubt, err toward simplicity, user control, and authentic voice capture.