Back to Editor

Technical Documentation

Editor: Decide with Clarity

Editor: Technical Documentation

Tech Stack, Architecture, and RevenueCat Implementation


1. Tech Stack Overview

LayerTechnologyPurpose
iOS AppSwift / SwiftUINative iOS client (iOS 26+)
BackendPython / FastAPIAPI server, LLM orchestration
LLM ProvidersOpenAI, Google GeminiAI reasoning
MonetizationRevenueCatSubscription management
IntegrationsNotion APIArtifact export
PersistenceUserDefaults (iOS), JSON filesLocal storage

iOS App

  • Language: Swift 5.9+
  • UI Framework: SwiftUI with iOS 26 Liquid Glass effects
  • Min Target: iOS 26
  • Architecture: Observable pattern with centralized router state machine

Backend

  • Framework: FastAPI (Python 3.9+)
  • Endpoints: 6 REST endpoints (including Notion OAuth)
  • LLM Integration: Provider abstraction layer (OpenAI, Gemini)
  • Auth: Bearer token authentication

2. Architecture

2.1 High-Level Flow

┌─────────────────────────────────────────────────────────────┐
│                         iOS App                             │
├─────────────────────────────────────────────────────────────┤
│  ElevatorView (vertical paging navigation)                  │
│    ├── Settings Floor (-1)                                  │
│    ├── Start Session Floor (0) ← default                    │
│    └── Library Floor (1)                                    │
├─────────────────────────────────────────────────────────────┤
│  AppRouter (state machine)                                  │
│    ├── flowState: elevator | modeSelection | guidedInput |  │
│    │              thinkingOutput | sessionComplete | ...    │
│    ├── currentSession: ThinkingSession?                     │
│    └── paywallContext: PaywallContext?                      │
├─────────────────────────────────────────────────────────────┤
│  Services                                                   │
│    ├── SubscriptionService (RevenueCat)                     │
│    ├── APIClient (backend communication)                    │
│    ├── SessionStore (persistence)                           │
│    ├── ProfileStore (user context)                          │
│    └── NotionService (artifact export)                      │
└──────────────────────────────┬──────────────────────────────┘
                               │ HTTPS
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                      FastAPI Backend                        │
├─────────────────────────────────────────────────────────────┤
│  Endpoints                                                  │
│    ├── GET  /v1/health              (health check)          │
│    ├── GET  /v1/models              (available models)      │
│    ├── POST /v1/chat                (thinking session)      │
│    ├── POST /v1/onboarding          (onboarding flow)       │
│    ├── POST /v1/extract             (profile extraction)    │
│    └── GET  /v1/notion/oauth-redirect (Notion OAuth)        │
├─────────────────────────────────────────────────────────────┤
│  Prompt Assembly                                            │
│    ├── Persona blocks (Editor, Operator, Strategist)        │
│    ├── Mode blocks (5 thinking modes)                       │
│    ├── Memory blocks (Free: stateless, Pro: contextual)     │
│    └── Output schema (structured JSON response)             │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                      LLM Providers                          │
│    ├── OpenAI (gpt-4o-mini)                                 │
│    └── Google (gemini-flash-lite-latest)                    │
└─────────────────────────────────────────────────────────────┘

2.2 Router State Machine

The AppRouter is the single source of truth for navigation and session state:

enum FlowState {
    case elevator(ElevatorStage)      // Main navigation
    case modeSelection                // Choosing thinking mode
    case guidedInput(ThinkingMode)    // Structured input form
    case thinkingOutput(sessionId: UUID)  // AI response
    case sessionComplete(sessionId: UUID) // Hard stop
    case artifact(...)                // Artifact generation
}

Key principle: No chat view. Users follow a structured flow:

  1. Select thinking mode
  2. Fill guided input form
  3. Receive structured output
  4. Session ends (hard stop)

2.3 Elevator Navigation

Vertical paging replaces traditional tab bar:

FloorViewPurpose
-1SettingsStageViewSubscription, legal, about
0SessionsHomeViewStart new session (default)
+1LibraryViewPast sessions and artifacts

Swipe up/down to navigate between floors.


3. Thinking Modes

Editor uses structured thinking modes instead of free-form chat. Each mode has a specific goal, required questions, and mandatory output structure.

3.1 Mode Overview

ModeTierGoal
Review an IdeaFreeSharpen idea until concrete, scoped, testable
Get UnstuckFreeIdentify real blocker and unblock progress
Make a DecisionProMake clear decision with explicit trade-offs
PrioritizeProSelect highest-leverage work
Trade-offs / StrategyProEvaluate strategy with risks and second-order effects

3.2 Mode Details

Review an Idea (Free)

  • Entry questions: Who is this for? What problem? Smallest shippable version?
  • Workflow: Clarify → Reduce scope → Identify weak points → Tighten
  • Output: Standard schema

Get Unstuck (Free)

  • Entry questions: Where stuck? What tried? What could do in 30 minutes?
  • Workflow: Name blocker → Remove false complexity → Propose one move
  • Additional output: Named blocker (e.g., "Fear of committing")

Make a Decision (Pro)

  • Entry questions: What decision? What options? Optimizing for?
  • Workflow: Compare options → Surface trade-offs → Recommend one
  • Additional output: "What would change my mind" section

Prioritize (Pro)

  • Entry questions: Items to prioritize? Deadline? Primary objective?
  • Workflow: Rank by leverage/urgency → Cut low-impact items
  • Additional output: Cut/Defer list

Trade-offs / Strategy (Pro)

  • Entry questions: Current strategy? Main risk? Success in 3 months?
  • Workflow: Generate options → Evaluate consequences → State the bet
  • Additional output: Bet statement ("We are betting that...")

4. Coach Personas

Personas are policy layers, not personality skins. Changing the persona changes reasoning, not just tone.

4.1 The Editor (Free + Pro)

AttributeValue
RoleClarity, simplification, reduction of noise
BiasReduce > Expand, Precision > Exploration
VoiceCalm, editorial, precise, minimal words

Always: Ask "What are we really deciding?", identify core constraint, compress to one sentence

Never: Over-explain, add motivational language, explore tangents

4.2 The Operator (Pro Only)

AttributeValue
RoleExecution, systems, throughput
BiasAction > Discussion, Shipping > Polishing
VoicePractical, direct, slightly impatient

Always: Ask for deadlines, define Definition of Done, convert to weekly/daily actions

Never: Stay at vision level, accept vague plans, assume unlimited time

4.3 The Strategist (Pro Only)

AttributeValue
RoleTrade-offs, long-term thinking, decision quality
BiasLong-term > Short-term, Explicit trade-offs > Ambiguity
VoiceAnalytical, calm, demanding

Always: Ask what optimizing for, surface risks, provide 2-3 options with consequences

Never: Say "it depends" without structure, avoid choosing when required


5. Prompt Generation

5.1 Prompt Assembly Architecture

The system prompt is assembled dynamically from structured blocks, never written as a monolithic string.

┌─────────────────────────────────────────────┐
│           FINAL SYSTEM PROMPT               │
├─────────────────────────────────────────────┤
│  1. Base System Prompt (always included)    │
│  2. Coach Persona Block (one of three)      │
│  3. Thinking Mode Block (one of five)       │
│  4. Output Schema Block (mandatory format)  │
│  5. Memory Block (tier-dependent)           │
└─────────────────────────────────────────────┘

5.2 Base System Prompt

Always included, defines global rules:

You are an AI thinking partner, not a chatbot.

GLOBAL RULES:
- No motivational language
- No emotional reassurance
- No emojis
- No generic advice
- Prefer short sentences and bullet points
- Always drive toward a concrete outcome

You must:
- Challenge assumptions
- Surface trade-offs
- End every session with a recommendation and next actions

Never end with:
- "Let me know if you want more"
- Open-ended invitations to continue

5.3 Persona Block Injection

One persona block is injected per session:

# Backend: assembler.py
if persona == "editor":
    blocks.append(EDITOR_PERSONA_BLOCK)
elif persona == "operator":
    blocks.append(OPERATOR_PERSONA_BLOCK)
elif persona == "strategist":
    blocks.append(STRATEGIST_PERSONA_BLOCK)

5.4 Mode Block Injection

One thinking mode block is injected per session:

if mode == "reviewIdea":
    blocks.append(REVIEW_IDEA_MODE_BLOCK)
elif mode == "getUnstuck":
    blocks.append(GET_UNSTUCK_MODE_BLOCK)
# ... etc

5.5 Tier Validation

Access is validated before calling the LLM:

# Backend tier validation
if tier == "free":
    if persona not in ["editor"]:
        raise HTTPException(403, "Coach requires Unlimited")
    if mode not in ["reviewIdea", "getUnstuck"]:
        raise HTTPException(403, "Mode requires Unlimited")

6. AI Input/Output Schema

6.1 Chat Request (iOS → Backend)

{
  "messages": [
    {"role": "user", "content": "...user input..."}
  ],
  "persona": "editor",
  "mode": "reviewIdea",
  "tier": "free",
  "user_context": null
}

For Pro users with memory:

{
  "messages": [...],
  "persona": "strategist",
  "mode": "makeDecision",
  "tier": "pro",
  "user_context": {
    "goals": ["Launch MVP by Q2", "Validate pricing model"],
    "constraints": ["Solo founder", "Limited runway"],
    "principles": ["Speed over perfection"],
    "past_insights": ["Previous pivot was too slow"]
  }
}

6.2 Mandatory Output Schema

Every AI response must follow this structure:

{
  "problem": "One-sentence problem statement",
  "constraints": ["Constraint 1", "Constraint 2"],
  "options": [
    {
      "name": "Option A",
      "description": "...",
      "pros": ["..."],
      "cons": ["..."]
    }
  ],
  "recommendation": "Clear recommendation with rationale",
  "next_actions": [
    "Action 1",
    "Action 2",
    "Action 3"
  ],
  "assumptions": "Explicit assumptions being made",
  "confidence": "Medium - needs user validation"
}

6.3 Chat Response (Backend → iOS)

{
  "id": "chat_abc123",
  "provider": "gemini",
  "model": "gemini-flash-lite-latest",
  "content": "{...structured JSON output...}",
  "usage": {
    "prompt_tokens": 1250,
    "completion_tokens": 890,
    "total_tokens": 2140
  },
  "latency_ms": 2340
}

6.4 Output Parsing (iOS)

The iOS app parses the structured JSON response:

private func parseJSONOutput(_ content: String) -> SessionOutput? {
    // Extract JSON from response
    guard let data = jsonString.data(using: .utf8),
          let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
    else { return nil }

    var output = SessionOutput(rawContent: content)
    output.problem = json["problem"] as? String ?? ""
    output.recommendation = json["recommendation"] as? String ?? ""
    // ... parse other fields

    return output
}

6.5 Quality Rules

A response is valid if it includes all schema sections.

A session is successful if:

  • Output could be used immediately
  • Recommendation is clear
  • User knows what to do next

A session fails if:

  • Feels like conversation
  • Avoids making a call
  • Produces no action

7. RevenueCat Implementation

7.1 Configuration

// SubscriptionService.swift
private let apiKey = "appl_xxxxxxxxxxxxxxxxxxxxx"  // RevenueCat public key
private let unlimitedEntitlementId = "unlimited"

RevenueCat is initialized on app launch:

func configure() {
    Purchases.configure(withAPIKey: apiKey)
    Task { await refreshStatus() }
}

7.2 Subscription Model

Single entitlement: unlimited

When active, unlocks all premium features:

  • Unlimited sessions
  • All 3 coaches
  • All 5 thinking modes
  • Long-term memory
  • Export outputs
// Checking subscription status
var isPro: Bool {
    tier == .pro
}

private func updateTier(from customerInfo: CustomerInfo) {
    if customerInfo.entitlements[unlimitedEntitlementId]?.isActive == true {
        tier = .pro
    } else {
        tier = .free
    }
}

7.3 Feature Gating

All gating uses the isPro property:

// Before starting a session
func startNewSession() {
    if !subscriptionService.isPro && !profileStore.canStartSession(isPro: false) {
        presentPaywall(.sessionLimitReached)
        return
    }
    // ... continue
}

// Before selecting a Pro-only mode
func selectMode(_ mode: ThinkingMode) {
    if mode.requiresPro && !subscriptionService.isPro {
        presentPaywall(.modeLocked(mode))
        return
    }
    // ... continue
}

7.4 Paywall Integration

Paywalls are context-aware:

enum PaywallContext {
    case sessionLimitReached
    case modeLocked(ThinkingMode)
    case coachLocked(CoachPersona)
    case artifactLocked
    case historyLocked
    case exportLocked
    case generic
}

Each context triggers a tailored paywall message.

7.5 Purchase Flow

func purchase(package: Package) async throws {
    let result = try await Purchases.shared.purchase(package: package)
    updateTier(from: result.customerInfo)
}

func restorePurchases() async throws {
    let customerInfo = try await Purchases.shared.restorePurchases()
    updateTier(from: customerInfo)
}

7.6 Subscription Management

In-app subscription management uses RevenueCat's CustomerCenterView:

// SettingsStageView.swift
@State private var showingCustomerCenter = false

Button("Manage Subscription") {
    showingCustomerCenter = true
}
.sheet(isPresented: $showingCustomerCenter) {
    CustomerCenterView()
}

8. Session Limit Enforcement

Free users are limited to 7 sessions per month:

// ProfileStore.swift
func canStartSession(isPro: Bool) -> Bool {
    if isPro { return true }
    resetSessionCountIfNewMonth()
    return profile.monthlySessionCount < FreeTierLimits.monthlySessionLimit
}

Session count is incremented only when output is generated (not on mode selection):

// AppRouter.swift - setOutput()
let isFirstOutput = session.output == nil
if isFirstOutput && !subscriptionService.isPro {
    profileStore.incrementSessionCount()
}

This prevents counting abandoned sessions.


9. Memory System (Pro Only)

9.1 Profile Extraction

After each session, the backend extracts insights:

// AppRouter.swift
if isFirstOutput && subscriptionService.isPro {
    Task {
        await extractProfileInsights(from: session, output: output)
    }
}

Extracted data includes:

  • Goals (with status)
  • Blockers
  • Values
  • Commitments
  • Insights
  • Themes

9.2 Memory Injection

Pro users get context injected into each session:

// ThinkingOutputView.swift
let userContext: UserContextPayload? = subscriptionService.isPro
    ? UserContextPayload.from(profileStore.profile)
    : nil

let response = try await APIClient.shared.chat(
    messages: [...],
    userContext: userContext  // Injected for Pro
)

9.3 Backend Memory Blocks

The backend assembles different memory blocks based on tier:

Free tier:

MEMORY RULE:
You have no long-term memory.
Treat this session as stateless.
Do not reference past conversations.

Pro tier:

USER CONTEXT:
- Current goals: [...]
- Constraints: [...]
- Principles: [...]

RELEVANT PAST INSIGHTS:
- [insight 1]
- [insight 2]

Use this context to improve reasoning and consistency.

10. Notion Integration (Pro Only)

10.1 Overview

Unlimited users can export artifacts directly to their Notion workspace. This keeps decisions connected to existing workflows without copy-pasting.

10.2 OAuth Flow

The integration uses Notion's OAuth 2.0 flow:

  1. User taps "Connect" in Settings
  2. App opens Notion authorization in ASWebAuthenticationSession
  3. Notion redirects to backend (/v1/notion/oauth-redirect)
  4. Backend exchanges code for access token
  5. Backend redirects to app with token via custom URL scheme
  6. Token is stored securely in iOS Keychain
iOS App                    Backend                      Notion
   │                          │                            │
   │──── Open OAuth URL ──────┼────────────────────────────▶
   │                          │                            │
   │◀───────────────────── Redirect with code ─────────────│
   │                          │                            │
   │                          │◀─── Exchange code ─────────│
   │                          │──── Return token ──────────▶
   │                          │                            │
   │◀─── Redirect to app ─────│                            │
   │     (editor://notion-callback?token=xxx)              │
   │                          │                            │
   └── Store token in Keychain                             │

10.3 Artifact Export

When exporting, the app creates a Notion page with rich blocks:

// NotionService.swift
func saveArtifact(_ artifact: Artifact) async throws {
    let blocks = buildBlocks(for: artifact)
    try await createPage(title: artifact.title, blocks: blocks)
}

Block types used:

  • Callouts: Problem statement, recommendation
  • Headings: Section headers
  • Bulleted lists: Constraints, options, actions
  • To-do items: Next actions (checkable)

10.4 Security

  • OAuth client secret stored only on backend (never in app)
  • Access token stored in iOS Keychain (encrypted)
  • Users can disconnect at any time in Settings
  • No Notion data is stored on our servers

11. Testing

LayerTestingFramework
BackendAutomated test suitepytest
iOSAutomated test suiteSwift Testing

Backend tests cover:

  • Endpoint responses
  • Prompt assembly
  • Tier validation
  • Output parsing

iOS tests cover:

  • Model encoding/decoding
  • Tier restrictions
  • Artifact generation
  • JSON parsing

12. Deployment

ComponentEnvironment
BackendVPS (Docker + Caddy)
iOSApp Store (TestFlight)
RevenueCatProduction dashboard

Backend URL: https://api.aicoach.framara.net


Summary

Editor is built with:

  • SwiftUI for a native iOS experience
  • FastAPI for flexible LLM orchestration
  • RevenueCat for simple, reliable subscription management
  • Notion API for artifact export integration

The architecture enforces the product philosophy:

  • Router state machine prevents "chat creep"
  • Single entitlement simplifies monetization
  • Memory extraction creates value for Pro users
  • Feature gating happens before API calls, not after
  • Notion integration keeps decisions connected to workflows

RevenueCat is the single source of truth for entitlements, enabling clean separation between free and unlimited experiences.

© 2026 Editor. All rights reserved.