7  Multi-Platform Distribution

Moving beyond traditional Python package publishing, this chapter explores how to distribute complete applications to users across web, desktop, and mobile platforms—all from a single codebase. We’ll focus on an architecture pattern that works exceptionally well with AI-assisted development: FastAPI backend + React frontend + multiple distribution targets.

NoteA Note on Technologies Beyond Python

This chapter introduces React and Electron—technologies outside Python’s ecosystem. Don’t worry: the goal isn’t to master these tools, but to understand the architecture that lets you ship to multiple platforms. Your Python skills remain central (the backend is still FastAPI), and the companion templates handle the frontend implementation details. With AI assistance, you can work effectively with React and Electron by understanding their role in the architecture rather than memorizing their APIs.

7.1 The Modern Distribution Challenge

Today’s users expect applications everywhere:

  • Web: Accessible from any browser, no installation required
  • Desktop: Native experience on Windows, macOS, and Linux
  • Mobile: Installable apps or Progressive Web Apps (PWAs)

For indie developers and small teams, maintaining separate codebases for each platform is impractical. The solution is an API-first architecture where a single backend serves multiple frontends, and those frontends can be packaged for different platforms.

7.1.1 What We’re Building

By the end of this chapter, you’ll understand how to structure a project that can ship to:

  1. Web: Docker container deployable to any cloud platform
  2. PWA: Installable web app with offline capabilities
  3. Desktop: Native applications for Windows, macOS, and Linux via Electron

All from a single codebase, with automated builds through GitHub Actions.

7.2 Why FastAPI + React?

This isn’t about which technologies are theoretically “best”—it’s about which combination is most practical for AI-assisted development in 2025 and beyond.

7.2.1 Training Data Density

Python and JavaScript/React represent the largest pools of training data for AI models. This means:

  • Better code generation: AI has seen more examples of these patterns
  • More accurate suggestions: Edge cases are better handled
  • Richer ecosystem knowledge: AI understands popular libraries deeply

When you ask AI to help with a FastAPI endpoint or a React component, you’re working with the technologies AI knows best.

7.2.2 Ecosystem Maturity

Both ecosystems have:

  • Solved most common problems: Authentication, forms, state management, API design
  • Extensive documentation: AI can reference official docs and community resources
  • Active communities: Stack Overflow answers, GitHub issues, blog posts

This maturity translates directly to better AI assistance.

7.2.3 Clean Separation

The API-first approach creates natural boundaries:

┌─────────────────┐         ┌─────────────────┐
│  React Frontend │  ←───→  │ FastAPI Backend │
│  (TypeScript)   │   API   │    (Python)     │
└─────────────────┘         └─────────────────┘

This separation means:

  • AI can work on backend or frontend independently
  • Changes to one side don’t break the other (if the API contract holds)
  • Different team members (or AI sessions) can work in parallel
  • Testing is straightforward at each boundary

7.3 The Architecture Pattern

7.3.1 Project Structure for AI Collaboration

Here’s the recommended structure for a multi-platform application:

my-app/
├── backend/                    # FastAPI backend (Python)
│   ├── src/
│   │   └── my_app/
│   │       ├── __init__.py
│   │       ├── main.py         # FastAPI application
│   │       ├── api/            # API routes
│   │       │   ├── __init__.py
│   │       │   └── routes.py
│   │       ├── models/         # Pydantic models
│   │       │   ├── __init__.py
│   │       │   └── schemas.py
│   │       └── services/       # Business logic
│   │           ├── __init__.py
│   │           └── core.py
│   ├── tests/
│   ├── pyproject.toml
│   └── Dockerfile
│
├── frontend/                   # React frontend (TypeScript)
│   ├── src/
│   │   ├── components/         # React components
│   │   ├── hooks/              # Custom hooks
│   │   ├── services/           # API client
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── public/
│   ├── package.json
│   ├── vite.config.ts
│   └── Dockerfile
│
├── electron/                   # Desktop wrapper
│   ├── main.js                 # Electron main process
│   ├── preload.js              # Preload script
│   └── package.json
│
├── docker/                     # Container configurations
│   ├── docker-compose.yml      # Development environment
│   ├── docker-compose.prod.yml # Production configuration
│   └── Dockerfile.combined     # Single container for deployment
│
├── .github/
│   └── workflows/
│       ├── ci.yml              # Test on push
│       ├── build.yml           # Build all platforms
│       └── release.yml         # Publish releases
│
├── CLAUDE.md                   # AI context file
└── README.md

This structure provides clear boundaries that both humans and AI can navigate effectively.

7.3.2 The CLAUDE.md Pattern

A key practice for AI-assisted development is maintaining a CLAUDE.md file (or similar context file) at the project root. This file provides AI assistants with project-specific context:

# Project: My Application

## Architecture
- Backend: FastAPI (Python 3.11+)
- Frontend: React 18 + TypeScript + Vite
- Desktop: Electron
- Database: SQLite (development), PostgreSQL (production)

## Conventions
- Backend API routes follow RESTful conventions
- All API responses use Pydantic models
- Frontend components use functional style with hooks
- State management via React Query for server state

## Directory Structure
- `/backend/src/my_app/api/` - API route definitions
- `/backend/src/my_app/models/` - Pydantic schemas
- `/frontend/src/components/` - React components
- `/frontend/src/services/` - API client functions

## Development Commands
- Backend: `cd backend && uv run uvicorn my_app.main:app --reload`
- Frontend: `cd frontend && npm run dev`
- Both: `docker-compose up`

## Key Decisions
- Using SQLite for simplicity; PostgreSQL in production
- Electron for desktop instead of Tauri (more mature, better AI support)
- Single Docker container for deployment (backend serves frontend static files)

This file serves as a “briefing document” for AI assistants, reducing the need to repeatedly explain project context.

7.4 Building the Backend: FastAPI

FastAPI is an excellent choice for this architecture because it’s:

  • Type-native: Pydantic models provide automatic validation and documentation
  • Self-documenting: OpenAPI/Swagger docs are generated automatically
  • Async-capable: Handles concurrent requests efficiently
  • AI-friendly: Extensive training data and clear patterns

7.4.1 A Minimal FastAPI Backend

# backend/src/my_app/main.py
"""Main FastAPI application."""

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pathlib import Path

from .api.routes import router

app = FastAPI(
    title="My Application",
    description="API for my multi-platform application",
    version="1.0.0",
)

# CORS configuration for development
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Vite dev server
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# API routes
app.include_router(router, prefix="/api")

# Serve frontend static files in production
frontend_path = Path(__file__).parent.parent.parent.parent / "frontend" / "dist"
if frontend_path.exists():
    app.mount("/", StaticFiles(directory=frontend_path, html=True), name="frontend")

7.4.2 API Routes with Pydantic Models

# backend/src/my_app/api/routes.py
"""API route definitions."""

from fastapi import APIRouter, HTTPException
from typing import List

from ..models.schemas import Item, ItemCreate, ItemUpdate

router = APIRouter()

# In-memory storage for demonstration
items_db: dict[int, Item] = {}
next_id = 1


@router.get("/items", response_model=List[Item])
async def list_items():
    """List all items."""
    return list(items_db.values())


@router.post("/items", response_model=Item, status_code=201)
async def create_item(item: ItemCreate):
    """Create a new item."""
    global next_id
    new_item = Item(id=next_id, **item.model_dump())
    items_db[next_id] = new_item
    next_id += 1
    return new_item


@router.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: int):
    """Get a specific item by ID."""
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return items_db[item_id]


@router.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: int, item: ItemUpdate):
    """Update an existing item."""
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")

    stored_item = items_db[item_id]
    update_data = item.model_dump(exclude_unset=True)
    updated_item = stored_item.model_copy(update=update_data)
    items_db[item_id] = updated_item
    return updated_item


@router.delete("/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
    """Delete an item."""
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    del items_db[item_id]

7.4.3 Pydantic Models

# backend/src/my_app/models/schemas.py
"""Pydantic models for API request/response validation."""

from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional


class ItemBase(BaseModel):
    """Base item schema with common fields."""
    title: str = Field(..., min_length=1, max_length=100)
    description: Optional[str] = Field(None, max_length=500)
    completed: bool = False


class ItemCreate(ItemBase):
    """Schema for creating a new item."""
    pass


class ItemUpdate(BaseModel):
    """Schema for updating an item (all fields optional)."""
    title: Optional[str] = Field(None, min_length=1, max_length=100)
    description: Optional[str] = Field(None, max_length=500)
    completed: Optional[bool] = None


class Item(ItemBase):
    """Schema for item responses."""
    id: int
    created_at: datetime = Field(default_factory=datetime.utcnow)

    class Config:
        from_attributes = True

The type hints and Pydantic models serve double duty: they validate data at runtime AND they help AI generate more accurate code when working with your API.

7.5 Building the Frontend: React

The frontend consumes the FastAPI backend through a typed API client.

7.5.1 Project Setup with Vite

# Create React project with TypeScript
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm install @tanstack/react-query axios

7.5.2 API Client

// frontend/src/services/api.ts
import axios from 'axios';

const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Types matching backend Pydantic models
export interface Item {
  id: number;
  title: string;
  description: string | null;
  completed: boolean;
  created_at: string;
}

export interface ItemCreate {
  title: string;
  description?: string;
  completed?: boolean;
}

export interface ItemUpdate {
  title?: string;
  description?: string;
  completed?: boolean;
}

// API functions
export const itemsApi = {
  list: async (): Promise<Item[]> => {
    const response = await api.get('/items');
    return response.data;
  },

  get: async (id: number): Promise<Item> => {
    const response = await api.get(`/items/${id}`);
    return response.data;
  },

  create: async (item: ItemCreate): Promise<Item> => {
    const response = await api.post('/items', item);
    return response.data;
  },

  update: async (id: number, item: ItemUpdate): Promise<Item> => {
    const response = await api.put(`/items/${id}`, item);
    return response.data;
  },

  delete: async (id: number): Promise<void> => {
    await api.delete(`/items/${id}`);
  },
};

7.5.3 React Component with React Query

// frontend/src/components/ItemList.tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { itemsApi, Item, ItemCreate } from '../services/api';
import { useState } from 'react';

export function ItemList() {
  const queryClient = useQueryClient();
  const [newTitle, setNewTitle] = useState('');

  // Fetch items
  const { data: items, isLoading, error } = useQuery({
    queryKey: ['items'],
    queryFn: itemsApi.list,
  });

  // Create mutation
  const createMutation = useMutation({
    mutationFn: (item: ItemCreate) => itemsApi.create(item),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['items'] });
      setNewTitle('');
    },
  });

  // Toggle completion mutation
  const toggleMutation = useMutation({
    mutationFn: ({ id, completed }: { id: number; completed: boolean }) =>
      itemsApi.update(id, { completed }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['items'] });
    },
  });

  // Delete mutation
  const deleteMutation = useMutation({
    mutationFn: (id: number) => itemsApi.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['items'] });
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading items</div>;

  return (
    <div className="item-list">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (newTitle.trim()) {
            createMutation.mutate({ title: newTitle });
          }
        }}
      >
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="Add new item..."
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {items?.map((item: Item) => (
          <li key={item.id}>
            <input
              type="checkbox"
              checked={item.completed}
              onChange={() =>
                toggleMutation.mutate({
                  id: item.id,
                  completed: !item.completed,
                })
              }
            />
            <span className={item.completed ? 'completed' : ''}>
              {item.title}
            </span>
            <button onClick={() => deleteMutation.mutate(item.id)}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

7.6 Distribution Target: Web with Docker

The simplest distribution target is a containerized web application.

7.6.1 Single Container Deployment

For simpler deployments, combine backend and frontend in a single container:

# docker/Dockerfile.combined
# Multi-stage build for combined backend + frontend

# Stage 1: Build frontend
FROM node:20-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

# Stage 2: Production image
FROM python:3.11-slim
WORKDIR /app

# Install uv
RUN pip install uv

# Copy backend
COPY backend/pyproject.toml backend/uv.lock ./
RUN uv sync --frozen --no-dev

COPY backend/src ./src

# Copy built frontend
COPY --from=frontend-build /app/frontend/dist ./frontend/dist

# Expose port
EXPOSE 8000

# Run the application
CMD ["uv", "run", "uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"]

7.6.2 Docker Compose for Development

# docker/docker-compose.yml
version: '3.8'

services:
  backend:
    build:
      context: ../backend
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - ../backend/src:/app/src
    environment:
      - DEBUG=true
    command: uv run uvicorn my_app.main:app --reload --host 0.0.0.0

  frontend:
    build:
      context: ../frontend
      dockerfile: Dockerfile.dev
    ports:
      - "5173:5173"
    volumes:
      - ../frontend/src:/app/src
    environment:
      - VITE_API_URL=http://localhost:8000/api
    command: npm run dev -- --host

7.6.3 Cloud Deployment

The combined container can be deployed to any container platform:

# Build the production image
docker build -f docker/Dockerfile.combined -t my-app:latest .

# Push to registry (example: GitHub Container Registry)
docker tag my-app:latest ghcr.io/username/my-app:latest
docker push ghcr.io/username/my-app:latest

# Deploy to fly.io (example)
flyctl deploy

7.7 Distribution Target: Progressive Web App (PWA)

PWAs provide an app-like experience without requiring app store distribution.

7.7.1 Vite PWA Configuration

npm install vite-plugin-pwa -D
// frontend/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
      manifest: {
        name: 'My Application',
        short_name: 'MyApp',
        description: 'A multi-platform application',
        theme_color: '#ffffff',
        icons: [
          {
            src: 'pwa-192x192.png',
            sizes: '192x192',
            type: 'image/png',
          },
          {
            src: 'pwa-512x512.png',
            sizes: '512x512',
            type: 'image/png',
          },
        ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24, // 24 hours
              },
            },
          },
        ],
      },
    }),
  ],
});

7.7.2 When PWA Is Enough

Consider PWA when:

  • Your app is primarily web-based
  • Users have reliable internet (or you implement offline support)
  • You want to avoid app store review processes
  • You need quick deployment and updates

7.8 Distribution Target: Desktop with Electron

For a full native desktop experience, Electron wraps your web application in a native shell.

7.8.1 Why Electron for Indie Developers

Despite larger bundle sizes compared to alternatives like Tauri, Electron offers advantages for indie developers:

  1. Maturity: VS Code, Slack, Discord, and Figma prove it scales
  2. Auto-update: Built-in update mechanisms via electron-updater
  3. Training data: More examples mean better AI assistance
  4. Community: Extensive documentation and solved problems
  5. Consistency: Same behavior across platforms

7.8.2 Electron Project Structure

// electron/main.js
const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');
const { autoUpdater } = require('electron-updater');

// Handle creating/removing shortcuts on Windows when installing/uninstalling
if (require('electron-squirrel-startup')) {
  app.quit();
}

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  // In development, load from Vite dev server
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:5173');
    mainWindow.webContents.openDevTools();
  } else {
    // In production, load the built frontend
    mainWindow.loadFile(path.join(__dirname, '../frontend/dist/index.html'));
  }
}

app.whenReady().then(() => {
  createWindow();

  // Check for updates in production
  if (process.env.NODE_ENV !== 'development') {
    autoUpdater.checkForUpdatesAndNotify();
  }
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

// Auto-updater events
autoUpdater.on('update-available', () => {
  console.log('Update available');
});

autoUpdater.on('update-downloaded', () => {
  console.log('Update downloaded');
  // Optionally prompt user to restart
});

7.8.3 Preload Script

// electron/preload.js
const { contextBridge, ipcRenderer } = require('electron');

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('electronAPI', {
  getVersion: () => ipcRenderer.invoke('get-version'),
  platform: process.platform,
});

7.8.4 Electron Package Configuration

{
  "name": "my-app-desktop",
  "version": "1.0.0",
  "description": "My Application - Desktop",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder",
    "build:win": "electron-builder --win",
    "build:mac": "electron-builder --mac",
    "build:linux": "electron-builder --linux"
  },
  "build": {
    "appId": "com.example.myapp",
    "productName": "My Application",
    "directories": {
      "output": "dist"
    },
    "files": [
      "main.js",
      "preload.js",
      "../frontend/dist/**/*"
    ],
    "win": {
      "target": ["nsis", "portable"],
      "icon": "icons/icon.ico"
    },
    "mac": {
      "target": ["dmg", "zip"],
      "icon": "icons/icon.icns",
      "category": "public.app-category.productivity"
    },
    "linux": {
      "target": ["AppImage", "deb"],
      "icon": "icons",
      "category": "Utility"
    },
    "publish": {
      "provider": "github",
      "owner": "username",
      "repo": "my-app"
    }
  },
  "dependencies": {
    "electron-updater": "^6.1.0"
  },
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.0.0"
  }
}

7.8.5 Handling the Backend in Desktop Apps

For desktop apps, you have two options for the backend:

Option 1: Remote Backend The desktop app connects to a hosted API (same as the web version).

Option 2: Embedded Backend Bundle the Python backend with the Electron app and run it locally.

// electron/main.js - Embedded backend example
const { spawn } = require('child_process');
const path = require('path');

let backendProcess;

function startBackend() {
  const backendPath = path.join(__dirname, '../backend');

  backendProcess = spawn('python', ['-m', 'uvicorn', 'my_app.main:app', '--port', '8000'], {
    cwd: backendPath,
    env: { ...process.env, PYTHONUNBUFFERED: '1' },
  });

  backendProcess.stdout.on('data', (data) => {
    console.log(`Backend: ${data}`);
  });

  backendProcess.stderr.on('data', (data) => {
    console.error(`Backend error: ${data}`);
  });
}

app.whenReady().then(() => {
  startBackend();

  // Wait for backend to start, then create window
  setTimeout(createWindow, 2000);
});

app.on('before-quit', () => {
  if (backendProcess) {
    backendProcess.kill();
  }
});

For a more robust embedded backend, consider using PyInstaller to bundle Python into a standalone executable.

7.9 CI/CD for Multi-Platform Builds

GitHub Actions can automate building and releasing for all platforms.

7.9.1 Continuous Integration Workflow

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  backend-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install uv
        run: pip install uv

      - name: Install dependencies
        working-directory: ./backend
        run: uv sync --all-extras

      - name: Run tests
        working-directory: ./backend
        run: uv run pytest --cov

  frontend-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        working-directory: ./frontend
        run: npm ci

      - name: Run tests
        working-directory: ./frontend
        run: npm test

      - name: Build
        working-directory: ./frontend
        run: npm run build

7.9.2 Multi-Platform Build Workflow

# .github/workflows/build.yml
name: Build

on:
  push:
    tags:
      - 'v*'

jobs:
  build-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./docker/Dockerfile.combined
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}

  build-desktop:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install frontend dependencies
        working-directory: ./frontend
        run: npm ci

      - name: Build frontend
        working-directory: ./frontend
        run: npm run build

      - name: Install Electron dependencies
        working-directory: ./electron
        run: npm ci

      - name: Build Electron app
        working-directory: ./electron
        run: npm run build
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: desktop-${{ matrix.os }}
          path: electron/dist/*

  release:
    needs: [build-web, build-desktop]
    runs-on: ubuntu-latest
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            desktop-ubuntu-latest/*
            desktop-windows-latest/*
            desktop-macos-latest/*
          generate_release_notes: true

7.10 The Human’s Role in This Architecture

With this architecture in place, the human developer’s role shifts to:

7.10.1 Architecture Decisions

  • Defining the API contract (endpoints, data models)
  • Choosing the right distribution targets for the use case
  • Making tradeoffs between complexity and capability

7.10.2 Verification

  • Reviewing AI-generated code for correctness
  • Ensuring tests cover critical paths
  • Validating that the API contract is maintained

7.10.3 Orchestration

  • Managing the build and release pipeline
  • Coordinating between backend and frontend work
  • Handling platform-specific edge cases

7.10.4 User Experience

  • Making design decisions AI can’t make
  • Understanding user needs and requirements
  • Testing across platforms and environments

The AI handles implementation details—writing the FastAPI routes, React components, Docker configurations—while you focus on the architecture, verification, and delivery.

7.11 Summary

Multi-platform distribution is achievable for indie developers through:

  1. API-first architecture: FastAPI backend + React frontend with clear boundaries
  2. Container deployment: Docker for web distribution
  3. PWA capabilities: Installable web apps with offline support
  4. Electron packaging: Native desktop apps for Windows, macOS, and Linux
  5. Automated CI/CD: GitHub Actions for multi-platform builds

This architecture is deliberately simple—“simple but not simplistic”—because simplicity enables effective AI collaboration. Clear structures, typed interfaces, and well-defined boundaries let you leverage AI for implementation while maintaining control over the architecture and delivery.

In the next chapter, we’ll explore how to structure these templates and configurations for maximum reusability across projects.