# FastSvelte Documentation > FastSvelte is a FastAPI (Python) + SvelteKit starter kit: auth, payments, and multi-tenancy, plus AI-ready usage metering, per-plan credit allowances, and top-ups. Self-hosted, full source code, no platform lock-in. Includes generated TypeScript API clients via Orval, admin and user dashboards, and Docker Compose for local development. Supports both B2C and B2B (multi-tenant with organizations, roles, and invitations) modes. FastSvelte is a FastAPI (Python) + SvelteKit starter kit: auth, payments, and multi-tenancy, plus AI-ready usage metering, per-plan credit allowances, and top-ups. Self-hosted, full source code, no platform lock-in. Includes generated TypeScript API clients via Orval, admin and user dashboards, and Docker Compose for local development. Supports both B2C and B2B (multi-tenant with organizations, roles, and invitations) modes. For the full documentation content concatenated into a single file, see [llms-full.txt](https://docs.fastsvelte.dev/llms-full.txt). # Get Started # Quick Start [FastSvelte](https://fastsvelte.dev) is a fullstack SaaS starter kit built with **FastAPI** (Python) and **SvelteKit** (TypeScript). Get it running locally in a few minutes, then browse [Features](https://docs.fastsvelte.dev/features/authentication/index.md) for everything that's included. The interactive `init.py` script handles setup end to end. Clone the repo, run `init.py`, then create an admin account and start the servers: ```sh # 1. Clone, then drop the template remote git clone https://github.com/harunzafer/fastsvelte.git my-project cd my-project && git remote remove origin # 2. Interactive setup — sets up your project for you (--dry-run to preview) uv run init.py # 3. Create a system admin, then start the backend cd backend uv run scripts/create_admin.py uv run uvicorn app.main:app --reload # 4. In a new terminal, start the frontend cd frontend && npm run dev ``` Prerequisites You'll need [uv](https://docs.astral.sh/uv/#installation) to run `init.py` in the first place, plus **Node.js 22+** and **Docker** (running). `init.py` checks those two first and exits with install instructions if either is missing, so once `uv` is in place you can just run it and fix what it flags. Open **[localhost:5173](http://localhost:5173)** and log in with the admin you created. The backend and interactive API docs are at [localhost:8000/docs](http://localhost:8000/docs); the landing template runs at [localhost:5174](http://localhost:5174). ## Next steps - **[Guides](https://docs.fastsvelte.dev/guides/project-setup/index.md)** — set up your project, develop, and ship - **[Features](https://docs.fastsvelte.dev/features/authentication/index.md)** — auth, billing, AI, multi-tenancy, and more - **[Architecture](https://docs.fastsvelte.dev/reference/architecture/index.md)** · **[Deployment](https://docs.fastsvelte.dev/deployment/index.md)** # Guides # Project Setup After this guide you'll understand what `init.py` configured, where configuration lives, and how to onboard a teammate. ## What `init.py` does You run `init.py` once during [Quick Start](https://docs.fastsvelte.dev/index.md). It: - prompts for your app name, mode (`b2c` / `b2b`), and database; - generates the `.env` files across `backend/`, `frontend/`, `landing/`, and `backend/db/`; - starts Docker PostgreSQL and runs migrations (with seed data); - installs backend (`uv`) and frontend/landing (`npm`) dependencies; - generates the type-safe API client (see [Type-Safe API Client](https://docs.fastsvelte.dev/guides/orval/index.md)). Pass `--dry-run` to preview without making changes. In development the email provider defaults to `stub`, so nothing is sent — verification and invitation links appear as `[STUB EMAIL]` blocks in the backend logs (see [Transactional Email](https://docs.fastsvelte.dev/features/email/index.md)). Once setup works, you can safely delete `init.py`; it's only needed once. ## Onboarding teammates **Only the first developer runs `init.py`.** Everyone else starts from your committed, pre-configured repository. 1. **Push to your own repository:** ```bash git remote add origin git@github.com:your-org/your-project.git git push -u origin main ``` 1. **Teammates clone and configure:** - Clone your repository (not the FastSvelte template). - Install the [prerequisites](https://docs.fastsvelte.dev/index.md) (uv, Node.js 22+, Docker). - Create `.env` files by copying each `.env.example` (`backend/`, `frontend/`, `landing/`, `backend/db/`). - Start the database: `docker compose -f backend/docker-compose.yml up db -d` - Run migrations: `cd backend/db && ./sqitch.sh dev deploy` - Install deps: `uv sync` in `backend/`, `npm install` in `frontend/` and `landing/` - Create a local admin: `cd backend && uv run scripts/create_admin.py` Never commit `.env` files They contain secrets and are gitignored. Share values through a secure channel (1Password, Doppler, a shared secrets manager). See [Configuration](https://docs.fastsvelte.dev/reference/configuration/index.md) for the full variable reference. # Development Workflow After this guide you can make a change across the stack and run the app. Work in the **DB → Backend → Frontend** order so the API client regenerates against an up-to-date backend. ## Database changes ```bash cd backend/db ./sqitch.sh add feature_name -n "Description" # Edit the generated deploy/, revert/, verify/ SQL files ./sqitch.sh dev deploy ``` See [Database](https://docs.fastsvelte.dev/features/database/index.md) for the migration model. ## Backend changes ```bash cd backend uv add package_name # add a dependency (--dev for dev-only) uv run ruff check . # lint uv run pytest # test uv run uvicorn app.main:app --reload # run (auto-reloads on change) ``` ## Frontend changes After backend API changes, regenerate the typed client (see [Type-Safe API Client](https://docs.fastsvelte.dev/guides/orval/index.md)): ```bash cd frontend npm run generate # regenerate the API client (backend must be running) npm run format # Prettier npm run check # type checking npm run test # tests (also test:unit / test:e2e) npm run dev # dev server (hot reload) ``` # Tutorials This section provides step-by-step guides for extending [FastSvelte](https://fastsvelte.dev) with new functionality. ## Adding a New Entity (End-to-End) This tutorial walks through adding a complete "Projects" feature to demonstrate how to extend every layer of the FastSvelte stack. You'll learn to add database tables, backend APIs, and frontend interfaces. ### Overview We'll build a Projects feature that allows users to: - Create, read, update, and delete projects - Associate projects with the current organization - Display projects in a list and detail view ### Step 1: Database Schema First, create a new migration for the projects table. ```bash cd backend/db ./sqitch.sh add projects -n "Add projects table" ``` Edit the generated migration file `backend/db/deploy/projects.sql`: ```sql -- Deploy {schema_name}:projects to pg -- Replace {schema_name} with your actual schema name (set during init.py) BEGIN; CREATE TABLE IF NOT EXISTS {schema_name}.project ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, description TEXT, organization_id INTEGER NOT NULL REFERENCES {schema_name}.organization(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES {schema_name}."user"(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_project_organization ON {schema_name}.project(organization_id); CREATE INDEX IF NOT EXISTS idx_project_user ON {schema_name}.project(user_id); COMMIT; ``` Create the revert script `backend/db/revert/projects.sql`: ```sql -- Revert {schema_name}:projects from pg BEGIN; DROP INDEX IF EXISTS {schema_name}.idx_project_user; DROP INDEX IF EXISTS {schema_name}.idx_project_organization; DROP TABLE IF EXISTS {schema_name}.project; COMMIT; ``` Apply the migration: ```bash ./sqitch.sh dev deploy ``` ### Step 2: Backend Models Create the Pydantic models in `backend/app/model/project.py`: ```python from datetime import datetime from typing import Optional from pydantic import BaseModel class ProjectEntity(BaseModel): id: int name: str description: str | None = None organization_id: int user_id: int created_at: datetime updated_at: datetime class CreateProjectRequest(BaseModel): name: str description: str | None = None class UpdateProjectRequest(BaseModel): name: str | None = None description: str | None = None class ProjectResponse(BaseModel): id: int name: str description: str | None = None created_at: datetime updated_at: datetime ``` ### Step 3: Repository Layer Create `backend/app/data/repo/project_repo.py`: ```python from app.data.repo.base_repo import BaseRepo from app.model.project import ProjectEntity class ProjectRepo(BaseRepo): async def create_project( self, name: str, description: str | None, user_id: int, organization_id: int ) -> ProjectEntity: query = f""" INSERT INTO {self.schema}.project (name, description, user_id, organization_id) VALUES ($1, $2, $3, $4) RETURNING id, name, description, organization_id, user_id, created_at, updated_at """ row = await self.fetch_one(query, name, description, user_id, organization_id) return ProjectEntity(**row) async def get_project_by_id(self, project_id: int, organization_id: int) -> ProjectEntity | None: query = f""" SELECT id, name, description, organization_id, user_id, created_at, updated_at FROM {self.schema}.project WHERE id = $1 AND organization_id = $2 """ row = await self.fetch_one(query, project_id, organization_id) return ProjectEntity(**row) if row else None async def get_projects_by_organization(self, organization_id: int) -> list[ProjectEntity]: query = f""" SELECT id, name, description, organization_id, user_id, created_at, updated_at FROM {self.schema}.project WHERE organization_id = $1 ORDER BY created_at DESC """ rows = await self.fetch_all(query, organization_id) return [ProjectEntity(**row) for row in rows] async def update_project( self, project_id: int, name: str | None, description: str | None, organization_id: int ) -> ProjectEntity | None: query = f""" UPDATE {self.schema}.project SET name = COALESCE($3, name), description = COALESCE($4, description), updated_at = now() WHERE id = $1 AND organization_id = $2 RETURNING id, name, description, organization_id, user_id, created_at, updated_at """ row = await self.fetch_one(query, project_id, organization_id, name, description) return ProjectEntity(**row) if row else None async def delete_project(self, project_id: int, organization_id: int) -> None: query = f"DELETE FROM {self.schema}.project WHERE id = $1 AND organization_id = $2" await self.execute(query, project_id, organization_id) ``` ### Step 4: Service Layer Create `backend/app/service/project_service.py`: ```python from app.data.repo.project_repo import ProjectRepo from app.model.project import CreateProjectRequest, ProjectEntity, UpdateProjectRequest class ProjectService: def __init__(self, project_repo: ProjectRepo): self.project_repo = project_repo async def create_project( self, organization_id: int, user_id: int, data: CreateProjectRequest ) -> ProjectEntity: return await self.project_repo.create_project( data.name, data.description, user_id, organization_id ) async def list_projects(self, organization_id: int) -> list[ProjectEntity]: return await self.project_repo.get_projects_by_organization(organization_id) async def get_project( self, project_id: int, organization_id: int ) -> ProjectEntity | None: return await self.project_repo.get_project_by_id(project_id, organization_id) async def update_project( self, project_id: int, organization_id: int, data: UpdateProjectRequest ) -> ProjectEntity | None: return await self.project_repo.update_project( project_id, data.name, data.description, organization_id ) async def delete_project(self, project_id: int, organization_id: int) -> None: await self.project_repo.delete_project(project_id, organization_id) ``` ### Step 5: API Routes Create `backend/app/api/route/project_route.py`: ```python from app.api.middleware.auth_handler import min_role_required from app.config.container import Container from app.exception.common_exception import ResourceNotFound from app.model.project import CreateProjectRequest, ProjectResponse, UpdateProjectRequest from app.model.role_model import Role from app.model.user_model import CurrentUser from app.service.project_service import ProjectService from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends router = APIRouter() @router.post("", response_model=ProjectResponse, operation_id="createProject") @inject async def create_project( data: CreateProjectRequest, user: CurrentUser = Depends(min_role_required(Role.MEMBER)), project_service: ProjectService = Depends(Provide[Container.project_service]), ): project = await project_service.create_project( user.organization_id, user.id, data ) return ProjectResponse.model_validate(project.model_dump()) @router.get("", response_model=list[ProjectResponse], operation_id="listProjects") @inject async def list_projects( user: CurrentUser = Depends(min_role_required(Role.MEMBER)), project_service: ProjectService = Depends(Provide[Container.project_service]), ): projects = await project_service.list_projects(user.organization_id) return [ProjectResponse.model_validate(p.model_dump()) for p in projects] @router.get("/{project_id}", response_model=ProjectResponse, operation_id="getProject") @inject async def get_project( project_id: int, user: CurrentUser = Depends(min_role_required(Role.MEMBER)), project_service: ProjectService = Depends(Provide[Container.project_service]), ): project = await project_service.get_project(project_id, user.organization_id) if not project: raise ResourceNotFound("project", project_id) return ProjectResponse.model_validate(project.model_dump()) @router.put("/{project_id}", response_model=ProjectResponse, operation_id="updateProject") @inject async def update_project( project_id: int, data: UpdateProjectRequest, user: CurrentUser = Depends(min_role_required(Role.MEMBER)), project_service: ProjectService = Depends(Provide[Container.project_service]), ): project = await project_service.update_project( project_id, user.organization_id, data ) if not project: raise ResourceNotFound("project", project_id) return ProjectResponse.model_validate(project.model_dump()) @router.delete("/{project_id}", status_code=204, operation_id="deleteProject") @inject async def delete_project( project_id: int, user: CurrentUser = Depends(min_role_required(Role.MEMBER)), project_service: ProjectService = Depends(Provide[Container.project_service]), ): project = await project_service.get_project(project_id, user.organization_id) if not project: raise ResourceNotFound("project", project_id) await project_service.delete_project(project_id, user.organization_id) ``` ### Step 6: Dependency Injection Setup Update `backend/app/config/container.py` to include the new components: ```python # 1. Add to imports at the top of the file from app.data.repo.project_repo import ProjectRepo from app.service.project_service import ProjectService # 2. Add to the Container class in the repositories section (around line 47) project_repo = providers.Singleton(ProjectRepo, db_config=db_config) # 3. Add to the Container class in the services section (around line 157) project_service = providers.Factory( ProjectService, project_repo=project_repo, ) ``` ### Step 7: Register Routes Update `backend/app/api/router.py` to include the new routes: ```python # 1. Add to imports at the top from app.api.route.project_route import router as project_router # 2. Add to include_all_routers() function (around line 31) app.include_router(project_router, prefix="/projects", tags=["Projects"]) ``` ### Step 8: Test the Backend API Before building the frontend, test your new API endpoints to ensure they work correctly. **1. Start the backend server:** ```bash cd backend uv run uvicorn app.main:app --reload ``` **2. Create a test file** at `backend/http/14_projects.http`: ```http ### FastSvelte Projects API Tests ### Login first using 01_auth.http - cookies persist across all files ### Variables loaded from .env file in this directory @baseUrl = {{$dotenv BASE_URL}} ### ============================================ ### CREATE PROJECT ### ============================================ ### Create Project POST {{baseUrl}}/projects Content-Type: application/json { "name": "My First Project", "description": "A sample project for testing" } ### ### ============================================ ### LIST & GET PROJECTS ### ============================================ ### List All Projects GET {{baseUrl}}/projects ### ### Get Specific Project # @prompt projectId Project ID to retrieve GET {{baseUrl}}/projects/{{projectId}} ### ============================================ ### UPDATE PROJECT ### ============================================ ### Update Project # @prompt projectId Project ID to update PUT {{baseUrl}}/projects/{{projectId}} Content-Type: application/json { "name": "Updated Project Name", "description": "Updated description" } ### ============================================ ### DELETE PROJECT ### ============================================ ### Delete Project # @prompt projectId Project ID to delete DELETE {{baseUrl}}/projects/{{projectId}} ### ============================================ ### TEST ERROR CASES ### ============================================ ### Get non-existent project (should return 404) GET {{baseUrl}}/projects/99999 ### ### Update non-existent project (should return 404) PUT {{baseUrl}}/projects/99999 Content-Type: application/json { "name": "Updated", "description": "Updated description" } ### ### Delete non-existent project (should return 404) DELETE {{baseUrl}}/projects/99999 ``` **3. Authenticate first:** Before testing the Projects API, you need to log in to get an authenticated session. If you're using VSCode with the [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client): 1. Open `backend/http/01_auth.http` 1. Run the "Register" or "Login" request to authenticate 1. The session cookie will persist across all `.http` files 1. Now open `backend/http/14_projects.http` and run the requests **4. Test your API:** Click "Send Request" above each test in the `14_projects.http` file to test all CRUD operations. **Alternative: Test with curl:** ```bash # First, login and save the session cookie curl -X POST http://localhost:8000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email": "your-email@example.com", "password": "your-password"}' \ -c cookies.txt # Create a project (using saved cookies) curl -X POST http://localhost:8000/api/projects \ -H "Content-Type: application/json" \ -d '{"name": "My First Project", "description": "A sample project"}' \ -b cookies.txt # List all projects curl http://localhost:8000/api/projects -b cookies.txt # Get a specific project (replace 1 with actual ID) curl http://localhost:8000/api/projects/1 -b cookies.txt # Update a project (replace 1 with actual ID) curl -X PUT http://localhost:8000/api/projects/1 \ -H "Content-Type: application/json" \ -d '{"name": "Updated Name", "description": "Updated description"}' \ -b cookies.txt # Delete a project (replace 1 with actual ID) curl -X DELETE http://localhost:8000/api/projects/1 -b cookies.txt ``` View API Documentation Visit `http://localhost:8000/docs` to see the auto-generated OpenAPI documentation for all your endpoints, including the new Projects API. ### Step 9: Generate Frontend API Client After adding the backend routes, regenerate the TypeScript API client: ```bash cd frontend npm run generate ``` This creates the API functions in `src/lib/api/gen/` that you'll use in the frontend. ### Step 10: Build Frontend Components Create the projects list page at `frontend/src/routes/(protected)/projects/+page.svelte`: ```html

Projects

{#if loading}
{:else if error}
{error}
{:else if projects.length === 0}

No projects yet

{:else}
{#each projects as project}

{project.name}

{#if project.description}

{project.description}

{/if}

Created: {new Date(project.created_at).toLocaleDateString()}

{/each}
{/if}
``` Create the new project form at `frontend/src/routes/(protected)/projects/new/+page.svelte`: ```html

Create New Project

{#if error}
{error}
{/if}
{ e.preventDefault(); handleSubmit(); }} class="space-y-4">
``` ### Step 11: Add Navigation Update your main navigation to include the projects link. In your sidebar component, add: ```html
  • Projects
  • ``` ### Step 12: Test the Complete Feature **1. Start the database** (if not already running): ```bash docker compose up db -d ``` **2. Start the backend**: ```bash cd backend uv run uvicorn app.main:app --reload ``` **3. Start the frontend** (in a new terminal): ```bash cd frontend npm run dev ``` **4. Navigate to projects**: Visit `http://localhost:5173/projects` **5. Test CRUD operations**: - Create a new project - View the projects list - Edit existing projects - Delete projects ### Summary You've successfully added a complete "Projects" feature that demonstrates: - **Database design**: Migration with proper foreign keys and constraints - **Backend architecture**: Repository pattern, service layer, API routes - **Dependency injection**: Proper container configuration - **API design**: RESTful endpoints with proper HTTP status codes - **Frontend integration**: API client generation and UI components - **Security**: Organization-based data isolation and authentication This pattern can be applied to add any new entity to your FastSvelte application. The key principles are: 1. Start with the database schema 1. Build from the data layer up (repository → service → routes) 1. Configure dependency injection 1. Generate and use the API client on the frontend 1. Build UI components following the existing design patterns # Metering a Custom Feature After this guide you can put any action behind a per-plan usage limit — and, optionally, let credit top-ups extend it the way AI tokens do. See [Plans & Usage](https://docs.fastsvelte.dev/features/plans-and-usage/index.md) for the engine this builds on. ## 1. Define the feature Add it to both the `FeatureKey` enum and the `PlanFeatures` model in `backend/app/model/plan_model.py`: ```python class FeatureKey(str, Enum): MAX_NOTES = "max_notes" TOKEN_LIMIT = "token_limit" ENABLE_AI = "enable_ai" MAX_PROJECTS = "max_projects" # new class PlanFeatures(BaseModel): max_notes: int | None token_limit: int | None enable_ai: bool | None max_projects: int | None # new ``` ## 2. Set limits per plan Add `max_projects` to each plan's `features` map — via the admin Plans page or seed data (see [Plans & Usage](https://docs.fastsvelte.dev/features/plans-and-usage/#configuring-limits)). ## 3. Enforce the limit Wrap the action with the generic usage service — check before, record after: ```python if not await usage_service.check_quota_for(org_id, FeatureKey.MAX_PROJECTS, 1): raise QuotaExceeded(FeatureKey.MAX_PROJECTS) project = await project_service.create(...) await usage_service.update_usage(org_id, FeatureKey.MAX_PROJECTS, 1) ``` That's all it takes to meter a feature against the plan limit, reset each billing period. ## 4. (Optional) Let credits top it up Today only AI tokens fall back to the [credit balance](https://docs.fastsvelte.dev/features/ai-billing/index.md). To let credits extend another feature, mirror what `AiUsageBillingService` does: when the per-period limit is exhausted, check and debit the org's credit balance before blocking the action. Two caveats, since the credit system is currently AI-token-specific: - The balance is a single **token-denominated** pool. If you want a separate balance per feature (e.g. "project credits"), add a per-feature balance rather than reusing the token pool. - Decide the unit. Reusing the token pool only makes sense if your feature is denominated the same way. So: metering any feature is built in; credit top-ups for non-AI features are a small, deliberate extension. # Swapping the LLM Provider FastSvelte ships OpenAI as the default LLM, but no route, service, or billing code ever imports a provider SDK directly — they depend only on a small `LLMClient` seam (see [AI Integration](https://docs.fastsvelte.dev/features/ai/#the-llmclient-interface)). To switch providers you write one class implementing that interface and repoint the dependency-injection container at it. Nothing else in the backend changes. ## The contract `backend/app/service/llm_client.py`: ```python class LLMClient(Protocol): async def structured(self, messages: list[dict], model: Type[T]) -> tuple[T, TokenUsage]: ... def stream(self, messages: list[dict]) -> AsyncIterator[str | TokenUsage]: ... ``` - **`structured()`** returns a parsed Pydantic model plus the call's `TokenUsage`. - **`stream()`** yields plain-text chunks and, as its **final item**, the call's `TokenUsage` (the caller accumulates text for the UI and forwards that final usage to billing). `messages` is an OpenAI-style `list[dict]` (roles `system` / `user` / `assistant`); each adapter is responsible for translating it into its own provider's request shape. ## Claude (Anthropic) — a complete recipe Add `anthropic` to the backend dependencies, then create `backend/app/service/claude_client.py`: ```python from typing import AsyncIterator, Type, TypeVar from anthropic import AsyncAnthropic, NOT_GIVEN from pydantic import BaseModel from app.model.llm_usage_model import TokenUsage from app.service.llm_client import LLMClient T = TypeVar("T", bound=BaseModel) PROVIDER = "anthropic" def _split_system(messages: list[dict]) -> tuple[str | object, list[dict]]: # Anthropic takes the system prompt as a top-level arg, not a role in `messages`. system = next((m["content"] for m in messages if m["role"] == "system"), NOT_GIVEN) chat = [m for m in messages if m["role"] != "system"] return system, chat class ClaudeClient(LLMClient): def __init__(self, model: str, max_tokens: int = 4096, api_key: str | None = None): self.client = AsyncAnthropic(api_key=api_key) self.model = model self.max_tokens = max_tokens async def structured(self, messages: list[dict], model: Type[T]) -> tuple[T, TokenUsage]: system, chat = _split_system(messages) response = await self.client.messages.parse( model=self.model, max_tokens=self.max_tokens, system=system, messages=chat, output_format=model, # Pydantic class -> response.parsed_output ) return response.parsed_output, self._usage(response.usage) async def stream(self, messages: list[dict]) -> AsyncIterator[str | TokenUsage]: system, chat = _split_system(messages) async with self.client.messages.stream( model=self.model, max_tokens=self.max_tokens, system=system, messages=chat, ) as stream: async for text in stream.text_stream: yield text final = await stream.get_final_message() yield self._usage(final.usage) def _usage(self, usage) -> TokenUsage: # Anthropic reports input/output separately; total is the sum. return TokenUsage( provider=PROVIDER, model=self.model, input_tokens=usage.input_tokens, output_tokens=usage.output_tokens, total_tokens=usage.input_tokens + usage.output_tokens, ) ``` The shape mirrors the shipped `OpenAIClient`: `structured()` uses the SDK's structured-output helper (`messages.parse` with a Pydantic `output_format`), and `stream()` yields text deltas then a final `TokenUsage`. The only provider-specific work is lifting the `system` role out of `messages` (Anthropic takes it as a top-level argument) and supplying `max_tokens`, which Anthropic requires. ## Wire it in Point the dependency-injection container at the new class. In `backend/app/config/container.py`, swap the `openai_client` provider for a `claude_client` and inject it wherever `openai_client` was wired (the `llm_client=...` argument): ```python claude_client = providers.Singleton( ClaudeClient, model=settings.anthropic_model, # e.g. "claude-opus-4-8" api_key=settings.anthropic_api_key, ) ``` Add `anthropic_api_key` / `anthropic_model` to `Settings` (mirroring the existing OpenAI fields), which read `FS_ANTHROPIC_API_KEY` / `FS_ANTHROPIC_MODEL` from the environment. ## Add the pricing row Cost is computed from the `model_price` table, so **add a row for the Claude model under provider `anthropic`** or cost calculation fails — see [AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/#model-pricing). Current Claude pricing (per 1M tokens, input / output): | Model | Input / 1M | Output / 1M | | ------------------- | ---------- | ----------- | | `claude-opus-4-8` | $5.00 | $25.00 | | `claude-sonnet-4-6` | $3.00 | $15.00 | | `claude-haiku-4-5` | $1.00 | $5.00 | ## Other providers **Gemini, LiteLLM, or any other provider** follow the exact same shape: one class implementing `structured()` + `stream()`, mapping the provider's usage fields into `TokenUsage`, wired via `container.py`, with a matching `model_price` row. The copilot, billing, routes, and UI are untouched. ## Next steps - **[AI Integration](https://docs.fastsvelte.dev/features/ai/index.md)** — the `LLMClient` seam, the sample copilot, and streaming. - **[AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/index.md)** — how every call is metered and billed. # Replacing the Demo FastSvelte ships an AI copilot demo (note Improve / Summarize) to show the patterns. After this guide you can strip it and start clean on your own product. ## 1. Plan your application - What is your main domain model? (Projects, Tasks, Documents, …) - What actions can users perform? (create, edit, share, export, …) - What billing model will you use? (per-seat, usage-based, tiered, …) ## 2. Build your features Follow **[Adding a Feature](https://docs.fastsvelte.dev/guides/adding-a-feature/#adding-a-new-entity-end-to-end)** to add an entity end to end (schema → repository → service → route → UI), then apply the same pattern to your own domain. ## 3. Remove the demo code The AI billing/usage infrastructure (`llm_client.py`, `openai_client.py`, and the usage/credit services + repos) is reusable — keep it if you want AI features of your own (see [AI Copilot](https://docs.fastsvelte.dev/features/ai/index.md)). **Backend:** ```bash cd backend rm app/model/note_model.py app/model/copilot_model.py rm app/service/note_service.py app/service/copilot_service.py rm app/api/route/note_route.py rm app/data/repo/note_repo.py ``` **Frontend:** ```bash cd frontend rm -rf src/routes/(protected)/notes ``` **Database** — drop the note table: ```bash cd backend/db ./sqitch.sh add remove_note_table -n "Remove note demo table" ``` Edit `deploy/remove_note_table.sql`: ```sql -- Deploy fastsvelte:remove_note_table to pg BEGIN; DROP TABLE IF EXISTS fastsvelte.note CASCADE; COMMIT; ``` Fill `revert/remove_note_table.sql` with the original `CREATE TABLE` from `deploy/001_schema.sql`, then `./sqitch.sh dev deploy`. **Optional** — if you're not using AI at all: ```bash cd backend uv remove openai ``` # Type-Safe API Client (Orval) FastSvelte keeps the frontend and backend in sync with a TypeScript API client **generated from the backend's OpenAPI spec** by Orval. Change a Pydantic model or route, regenerate, and the TypeScript compiler catches any breakage at compile time. ## Regenerate after backend changes ```bash cd frontend npm run generate # reads the OpenAPI spec and regenerates the client (backend must be running) ``` Generated code lives in `frontend/src/lib/api/gen/` — **don't edit it by hand**; it's overwritten on every generate. ## How it works 1. Define Pydantic models and routes with `response_model` and `operation_id`: ```python @router.post("", response_model=NoteResponse, operation_id="createNote") async def create_note(data: CreateNoteRequest, ...): ... ``` - `response_model` → the response type in the OpenAPI spec - `operation_id` → the generated TypeScript function name 1. FastAPI generates the OpenAPI spec from the decorators and models. 1. Orval reads the spec and emits typed functions and models. 1. Import and use them with full type safety: ```typescript import { createNote } from "$lib/api/gen/notes"; import type { CreateNoteRequest, NoteResponse } from "$lib/api/gen/model"; const note: NoteResponse = await createNote({ title: "My Note", content: "..." }); ``` Change the backend → run `npm run generate` → the compiler flags any frontend code that no longer matches. # Upgrading Dependencies After this guide you can keep dependencies current and secure using Dependabot plus a verify workflow. ## Upgrade policy FastSvelte ships a [Dependabot config](https://github.com/) (`.github/dependabot.yml`) so routine upgrades are automated and reviewable: - **Automated, weekly:** Dependabot opens **grouped** PRs once a week per ecosystem — `npm` (frontend), `npm` (landing), and `uv` (backend). The backend uses a 7-day cooldown so brand-new releases settle before they're proposed. - **Minor & patch upgrades:** handled by those weekly PRs. Review and merge once CI passes (`backend.yml`, `frontend.yml`, `landing.yml`). - **Major versions:** **excluded** from Dependabot (`version-update:semver-major` is ignored) and done deliberately, one component at a time, since they may require code changes — follow the major-version workflow below. - **Security updates:** fast-track immediately, outside the weekly cadence. - **Version pinning:** `pyproject.toml` and `package.json` declare lower-bound (`>=`) ranges; the lockfiles (`uv.lock`, `package-lock.json`) pin exact versions and are committed — so installs are reproducible while ranges stay flexible. ## Backend dependencies (Python) FastSvelte uses `uv` to manage Python dependencies. ```bash cd backend # Upgrade all dependencies (or a specific one) uv sync --upgrade uv add --upgrade package_name # Verify compatibility uv run pytest ``` **Note:** Smoke tests in `backend/test/smoke/` verify app startup, database connectivity, and API functionality. Database tests require PostgreSQL running (`docker compose up db -d`) but skip gracefully if unavailable. ## Frontend dependencies (npm) The frontend and landing page use `package.json` + `package-lock.json`. ```bash # Frontend cd frontend npm outdated # check what's outdated npm update # update within semver ranges npm run build && npm run check && npm run lint && npm run test # Landing (simpler, no tests by default) cd ../landing npm outdated && npm update npm run build && npm run check && npm run lint ``` ### Major version upgrades ```bash npm install -g npm-check-updates # one-time cd frontend # or cd landing ncu # preview ncu -u # bump package.json to latest npm install npm run build && npm run check && npm run lint npm run test # frontend only (landing has no tests by default) ``` **Important:** - Commit `package-lock.json` after upgrades. - Test thoroughly after major updates, and upgrade one component at a time (backend, frontend, landing) for easier troubleshooting. - Frontend smoke tests live in `frontend/src/tests/smoke/` and run with `npm run test`. - To add tests to landing, run `npx sv add vitest`. # Getting Updates [FastSvelte](https://fastsvelte.dev) is yours to customize and extend. You can stay connected to receive bug fixes and improvements, or disconnect completely and own your codebase. ## Minimizing Conflicts **Extend rather than modify** to reduce merge conflicts. When you create new files instead of editing existing FastSvelte code, conflicts are limited to a few predictable integration points. **Extension Pattern for Fewer Conflicts:** - New routes, services, repositories in their own files - New frontend pages and components - New database migrations - Conflicts are still expected in files such as `container.py` or `routes.py` where new components are registered. But manageable. ## Update Methods ### 1. Merge (Recommended) Keep FastSvelte as an upstream remote and merge updates: ```bash # During initial setup (instead of removing origin) git clone https://github.com/harunzafer/fastsvelte.git my-project cd my-project git remote rename origin upstream git remote add origin git push -u origin main # If you already removed origin, add upstream back git remote add upstream https://github.com/harunzafer/fastsvelte.git # Later, when you want updates git fetch upstream # View available updates git log --oneline HEAD..upstream/main # Merge updates git merge upstream/main # Resolve conflicts if any (usually in container.py, routes.py) # After resolving, commit the merge # Test the changes cd backend && pytest # For backend changes cd frontend && npm run build && npm run test # For frontend changes cd landing && npm run build && npm run check # For landing page changes # Push to your repository git push origin main ``` ### 2. Rebase An alternative to merge that replays your commits on top of the upstream changes, resulting in a linear history: ```bash # Set up upstream remote (same as merge method) git remote add upstream https://github.com/harunzafer/fastsvelte.git # Fetch updates git fetch upstream # Rebase your branch onto upstream git rebase upstream/main # Resolve any conflicts, then continue git rebase --continue # Push to your repository (force required since history was rewritten) git push --force-with-lease origin main ``` **Best for:** Projects early in development with few commits, or when you want a clean linear history. **Caution:** Avoid rebasing if you have a shared branch with collaborators — it rewrites history. ### 3. Cherry-pick For selective updates or when you've modified core files extensively: ```bash # Cherry-pick specific commits git cherry-pick # Or a range of commits git cherry-pick .. # Resolve conflicts and continue git cherry-pick --continue ``` ### 4. Manual Copy Remove the FastSvelte remote and manually copy changes when needed: ```bash # During initial setup (default approach in Getting Started) git clone https://github.com/harunzafer/fastsvelte.git my-project cd my-project git remote remove origin git remote add origin git push -u origin main # Later, check GitHub for updates you want # Manually copy relevant changes to your project ``` **Best for:** Projects with heavy customization or teams that prefer full control. ## Best Practices 1. **Always test updates in development first** - Never update production directly 1. **Create a backup branch** before major updates 1. **Review release notes** to understand what changed ______________________________________________________________________ **Next Steps:** - [Development Guide](https://docs.fastsvelte.dev/guides/development-workflow/index.md) - Build your application - [Deployment](https://docs.fastsvelte.dev/deployment/index.md) - Deploy to production # Features # Authentication FastSvelte ships session-based authentication out of the box: email/password and Google OAuth login, email verification, password reset, and role-based access control. Sessions are server-side and carried in an HTTP-only cookie. ## Roles Roles are precedence-ordered (`backend/app/model/role_model.py`). Endpoints are guarded by `min_role_required(Role.X)` — a user satisfies the requirement if their role's precedence is **at least** the required role's. | Role | DB name | Precedence | | -------------- | ----------- | ---------- | | `READONLY` | `readonly` | 0 | | `MEMBER` | `member` | 1 | | `ORG_ADMIN` | `org_admin` | 2 | | `SYSTEM_ADMIN` | `sys_admin` | 3 | ## Sessions On login the backend creates a server-side session and sets a cookie (`backend/app/util/cookie_util.py`): - **Name** — `session_id` (configurable via `FS_SESSION_COOKIE_NAME`). - **`HttpOnly`** — always, so JavaScript can't read the token. - **`Secure`** — on in `beta`/`prod`, off in `dev`. - **`SameSite`** — `strict` in `beta`/`prod`, `lax` in `dev`. - **Max age** — `FS_SESSION_COOKIE_MAX_AGE` (default 24h). `POST /auth/logout` invalidates the session server-side and clears the cookie. Expired sessions are pruned by the cron job (`/cron`, gated by `FS_CRON_SECRET`; retention `FS_CRON_SESSION_RETENTION_DAYS`, default 7 days). ## Email & password | Endpoint | Purpose | | ----------------------- | ------------------------------------------------- | | `POST /auth/signup` | Create a user (B2C — see [modes](#b2c-vs-b2b)) | | `POST /auth/signup-org` | Create an organization + its admin (B2C) | | `POST /auth/login` | Log in; sets the session cookie | | `POST /password/forgot` | Email a password-reset link | | `POST /password/reset` | Reset the password with a token | | `POST /password/update` | Change the password while logged in (`READONLY`+) | Login requires a **verified email** — unverified accounts get `EmailNotVerified`. Password-reset emails are sent through the configured [email provider](https://docs.fastsvelte.dev/features/email/index.md). ## Google OAuth | Endpoint | Purpose | | -------------------------------------- | --------------------------------------------------------------- | | `GET /auth/oauth/google/authorize-url` | Returns the Google authorization URL | | `GET /auth/oauth/google/callback` | Handles the redirect, creates the session, redirects to the app | The flow uses a signed `state` parameter for CSRF protection (`backend/app/util/oauth_util.py`), validated on callback. Errors (cancelled, invalid state, etc.) redirect back to `/login?error=...`. For Google Cloud credentials and redirect-URI setup, see the [Integrations guide](https://docs.fastsvelte.dev/features/google-oauth/index.md); to add other providers, extend `oauth_util.py` and add routes in `auth_route.py`. ## B2C vs B2B Signup behavior depends on the app mode: - **B2C** — public `signup` and `signup-org` are open. - **B2B** — both return `404`; users join only via [organization invitations](https://docs.fastsvelte.dev/features/multi-tenancy/index.md). Only a `SYSTEM_ADMIN` creates organizations. See **[B2B Mode](https://docs.fastsvelte.dev/features/multi-tenancy/index.md)** for the full multi-tenant model. ## Configuration ```bash # Google OAuth FS_GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com" FS_GOOGLE_CLIENT_SECRET="GOCSPX-your-google-client-secret" # Signs the OAuth state parameter (CSRF protection) FS_JWT_SECRET_KEY="your-jwt-secret-key" # Sessions FS_SESSION_COOKIE_NAME="session_id" FS_SESSION_COOKIE_MAX_AGE=86400 # Session-cleanup cron FS_CRON_SECRET="your-secure-cron-secret" FS_CRON_SESSION_RETENTION_DAYS=7 ``` ## Next steps - **[Integrations](https://docs.fastsvelte.dev/features/google-oauth/index.md)** — Google OAuth provider setup and email configuration. - **[B2B Mode](https://docs.fastsvelte.dev/features/multi-tenancy/index.md)** — organizations, invitations, and roles. # Google OAuth FastSvelte supports social login via Google OAuth out of the box, alongside [email/password authentication](https://docs.fastsvelte.dev/features/authentication/index.md). The architecture extends to other providers. ## Setup 1. Go to [console.cloud.google.com](https://console.cloud.google.com) and create (or select) a project. 1. Enable the Google OAuth2 API. 1. Create an **OAuth 2.0 Client ID** (Web application). 1. Add the authorized redirect URI: `http://localhost:8000/auth/oauth/google/callback` (and your production equivalent). JWT secret for OAuth state OAuth flows use a JWT secret to sign the `state` parameter (CSRF protection). `init.py` auto-generates it. To generate manually: ```bash python -c "import secrets; print(secrets.token_urlsafe(32))" ``` ## Configuration ```bash # backend/.env FS_GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com" FS_GOOGLE_CLIENT_SECRET="GOCSPX-your-google-client-secret" # Signs the OAuth state parameter (CSRF protection) FS_JWT_SECRET_KEY="your-jwt-secret-key-here" ``` ## The Login Flow 1. **Frontend** requests `GET /auth/oauth/google/authorize-url`. 1. **Backend** returns the Google authorization URL (with a signed `state`). 1. **User** authenticates with Google. 1. **Google** redirects to `GET /auth/oauth/google/callback` with an authorization code. 1. **Backend** validates `state`, exchanges the code, creates/loads the user, and sets the session cookie. 1. **Frontend** lands logged in. OAuth errors redirect to `/login?error=...`. ## Adding More Providers To add GitHub, Microsoft, etc.: 1. Extend the helpers in `backend/app/util/oauth_util.py`. 1. Add provider routes in `backend/app/api/route/auth_route.py`. 1. Add the provider credentials to `backend/app/config/settings.py`. ## Troubleshooting - **Redirect URI mismatch** — it must match the provider config exactly (scheme, host, port, path). - **Invalid client** — recheck the client ID/secret. - **HTTPS** — most providers require HTTPS redirect URIs in production. See **[Authentication](https://docs.fastsvelte.dev/features/authentication/index.md)** for sessions, roles, and the rest of the auth model. # Billing & Subscriptions FastSvelte handles **subscription billing** through Stripe's **Customer Portal** — no custom billing UI to build. Subscription state is **webhook-driven**: Stripe is the source of truth, and the backend updates the database only from webhook events sent to `/webhooks/stripe`. Three columns hold the link: `organization.stripe_customer_id`, `plan.stripe_product_id`, and `organization_plan` (status, period, subscription id). ## Setup ### 1. API key Create a Stripe account and a [sandbox](https://docs.stripe.com/sandboxes/dashboard/manage#create-a-sandbox) for development, then copy your **Secret key** (Developers → API Keys; `sk_test_...` in sandbox) into `backend/.env`: ```bash FS_STRIPE_API_KEY=sk_test_YOUR_KEY_HERE ``` Only the secret key is needed — the portal handles all payment UI. Never commit it. ### 2. Create products In **Products → Add Product**, create your tiers and copy each **Product ID** (`prod_...`): - **Free tier** — a single **monthly** price; must be **$0** (auto-provisioned on first login). - **Paid tiers** — **monthly** and **annual** prices in the same currency. No public free tier? You still need a $0 default product for auto-provisioning — name it "Default" and simply don't add it to the portal, so users can't select it. ### 3. Link products to plans The kit seeds three plans (Free, Professional, Premium). At **`/admin/plans`**, edit each and paste its **Stripe Product ID**. The default plan (Free) must map to your $0 product — it's validated and auto-assigned to new users. ### 4. Configure the Customer Portal In **Settings → Billing → Customer portal**: enable "customers can switch plans" and add the products/prices you want to offer; enable updating payment methods and viewing invoices, and optionally cancellation. Save. ## Local development Forward Stripe webhooks to your backend with the [Stripe CLI](https://docs.stripe.com/stripe-cli/install): ```bash stripe login stripe listen --forward-to localhost:8000/webhooks/stripe ``` Copy the printed `whsec_...` into `backend/.env`, then restart the backend (keep `stripe listen` running in a second terminal): ```bash FS_STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx ``` ## Testing Use Stripe [test cards](https://stripe.com/docs/testing) in sandbox (any future expiry, any CVC and postal code): | Card | Result | | --------------------- | --------- | | `4242 4242 4242 4242` | Success | | `4000 0000 0000 0002` | Declined | | `4000 0025 0000 3155` | 3D Secure | ## Going live In **Developers → Webhooks**, add an endpoint at `https://api.yourdomain.com/webhooks/stripe` subscribed to these events, and copy its signing secret to `FS_STRIPE_WEBHOOK_SECRET`: - `customer.subscription.created` / `updated` / `deleted` - `checkout.session.completed` — fulfills [AI credit-pack](https://docs.fastsvelte.dev/features/ai-billing/index.md) purchases Then switch to **Live mode**: recreate products with live pricing (free tier still $0), use live `sk_live_...` keys, update `/admin/plans` with the live Product IDs, configure the portal in live mode, and test a real upgrade. ## Troubleshooting **Free subscription didn't sync (dev).** If you logged in before `stripe listen` was running, the free subscription exists in Stripe but not your database ("No active plan found"). Fix it from `/billing` → **Manage Subscription** → in the portal click **Cancel subscription**, then **Don't cancel** — that fires `customer.subscription.updated` and syncs. (Or resend the original event from **Stripe → Events**.) ## Related [Plans & Usage](https://docs.fastsvelte.dev/features/plans-and-usage/index.md) · [AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/index.md) · [Deployment](https://docs.fastsvelte.dev/deployment/index.md) # Plans & Usage FastSvelte meters usage and enforces per-feature limits through one generic system. The same engine powers the note quota and the AI token allotment — and you can extend it to anything. ## How it works - **Plans define limits.** Each plan carries a `features` map (typed by `PlanFeatures` + the `FeatureKey` enum in `backend/app/model/plan_model.py`). The kit ships `max_notes`, `token_limit`, and `enable_ai`. - **Usage is metered per billing period.** `OrganizationUsageService` tracks each org's consumption per `FeatureKey` against the plan's limit, scoped to the current subscription period — it resets when the period rolls over. - **Limits are enforced.** Call `check_quota_for(...)` before an action and `update_usage(...)` after: ```python if not await usage_service.check_quota_for(org_id, FeatureKey.MAX_NOTES, 1): raise QuotaExceeded(FeatureKey.MAX_NOTES) # ... perform the action ... await usage_service.update_usage(org_id, FeatureKey.MAX_NOTES, 1) ``` The note demo does exactly this on create and delete. ## Configuring limits Set each plan's limits in its `features` map via the admin Plans page or seed data — see [Admin & User Dashboards](https://docs.fastsvelte.dev/features/admin-dashboards/index.md). How plans map to Stripe products is covered in [Billing & Subscriptions](https://docs.fastsvelte.dev/features/billing/index.md). ## Add your own metered feature Metering a new feature is a small change: add a `FeatureKey` + `PlanFeatures` field, set limits per plan, and wrap your action with `check_quota_for` / `update_usage`. Full walkthrough: **[Metering a Custom Feature](https://docs.fastsvelte.dev/guides/metering-a-custom-feature/index.md)**. ## AI usage The AI token allotment (`token_limit`) is just one metered feature — but it adds a paid **credit top-up** layer on top. See [AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/index.md). # AI Integration FastSvelte ships a working, end-to-end AI feature so you can build your own on top of it instead of from scratch: a small provider-agnostic `LLMClient` seam, an OpenAI implementation, and a sample **note copilot** (Improve / Summarize) that streams its output to the UI. Every call is metered and billed — see **[AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/index.md)**. ## Setup Configure the OpenAI key and model in `backend/.env`: ```bash FS_OPENAI_API_KEY="sk-proj-your-openai-api-key" FS_OPENAI_MODEL="gpt-5-mini" ``` `FS_OPENAI_MODEL` defaults to **`gpt-5-mini`**. The model name flows from settings into the client provider in `backend/app/config/container.py`: ```python openai_client = providers.Singleton( OpenAIClient, model=settings.openai_model, temperature=0.1, api_key=settings.openai_api_key, ) ``` Whatever model you set **must have a row in the `model_price` table**, or usage-cost calculation fails — see [AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/#model-pricing). ## The `LLMClient` interface App code never imports the OpenAI SDK directly. It depends on a small `Protocol` in `backend/app/service/llm_client.py`: ```python class LLMClient(Protocol): async def structured( self, messages: list[dict], model: Type[T] ) -> tuple[T, TokenUsage]: ... def stream( self, messages: list[dict] ) -> AsyncIterator[str | TokenUsage]: ... ``` - **`structured()`** returns a parsed Pydantic model plus the call's `TokenUsage`. Usage travels with every response because every call is billable. - **`stream()`** yields plain-text chunks and, as its **final item**, the call's `TokenUsage` (captured from the terminal stream event). The caller accumulates text for the UI and forwards that final `TokenUsage` to billing. This seam is the whole point: swap providers by writing another `LLMClient`, and nothing else in the backend changes. ## OpenAI implementation `backend/app/service/openai_client.py` implements `LLMClient` against the OpenAI **Responses API** (`client.responses.parse` for structured output, `client.responses.stream` for streaming) — not the older Chat Completions surface. Token usage is read from the response and returned as `TokenUsage(provider, model, input_tokens, output_tokens, total_tokens)`. ## The sample copilot `backend/app/service/copilot_service.py` exposes two actions, **Improve** and **Summarize**, each accepting an optional `tone`. They build a system+user message pair and return `llm_client.stream(...)`. They're surfaced on the note routes in `backend/app/api/route/note_route.py`: | Endpoint | Body | Role | Response | | ------------------------------------ | --------------------- | -------- | --------------------- | | `POST /notes/{id}/copilot/improve` | `{ "tone"?: string }` | `MEMBER` | streamed `text/plain` | | `POST /notes/{id}/copilot/summarize` | `{ "tone"?: string }` | `MEMBER` | streamed `text/plain` | Each handler loads the note (404 if missing), checks the org has AI capacity before streaming (estimated from the note length), then returns a `StreamingResponse`. The stream is wrapped by the billing service so the final usage is recorded automatically — see [AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/index.md). ## Streaming to the frontend The frontend consumes the `text/plain` stream incrementally in `frontend/src/lib/api/copilotStream.ts`, appending chunks as they arrive so the user sees output render live. The copilot toolbar wires the Improve / Summarize buttons to these calls. ## Adding your own AI action 1. Add a method to `CopilotService` (or a new service) that builds messages and calls `llm_client.stream(...)` or `.structured(...)`. 1. Add a route that checks capacity, then wraps the stream with `AiUsageBillingService.stream_and_record(...)` (streaming) or calls `record_llm_usage(...)` after a `structured()` call. 1. Regenerate the typed API client and wire the UI. Billing is not optional plumbing you add later — route every call through `AiUsageBillingService` so usage is metered consistently. ## Using a different provider OpenAI is the shipped implementation. To swap it, write one class implementing the `LLMClient` interface against your provider's SDK and point `container.py` at it — no route, service, billing, or UI code changes, since that seam is all the rest of the backend depends on. See **[Swapping the LLM Provider](https://docs.fastsvelte.dev/guides/swapping-llm-provider/index.md)** for a complete, drop-in Claude (Anthropic) recipe and the Gemini / LiteLLM pattern. ## Next steps - **[AI Usage & Credit Billing](https://docs.fastsvelte.dev/features/ai-billing/index.md)** — how calls are metered, the monthly allotment, credit packs, and usage reporting. - **[Stripe Integration](https://docs.fastsvelte.dev/features/billing/index.md)** — the subscription billing the AI allotment and credit purchases build on. # AI Usage & Credit Billing Every AI call is metered and billed against the **organization** (this kit is B2B multi-tenant; the org is the billing unit, which degrades naturally to "per user" for single-member orgs). Billing layers onto the existing [Stripe subscription](https://docs.fastsvelte.dev/features/billing/index.md) model — there is no parallel per-user credit system. A call's tokens are charged in a fixed order: 1. **Monthly allotment** — the plan's included token budget, resets each billing period. 1. **Credit-pack balance** — purchased, non-expiring tokens. 1. **Overage** — usage beyond both. **Blocked by default** (see [Overage settings](#overage-settings)). ## How a call is billed `backend/app/service/ai_usage_billing_service.py` owns this. Two entry points: - **`has_ai_capacity(org_id, estimated_tokens)`** — a pre-flight check the copilot routes call *before* streaming, so an org that's out of capacity gets a clean `QuotaExceeded` instead of a half-streamed answer. It estimates from input length (~4 chars/token). - **`stream_and_record(...)`** / **`record_llm_usage(...)`** — after the call, the actual `TokenUsage` is split across allotment → credits → overage, the consumed buckets are debited, the USD cost is computed (see [Model pricing](#model-pricing)), and a row is written to `llm_usage_log` tagged with the `bucket` it drew from. For streaming calls, `stream_and_record` forwards text chunks to the client and bills the final `TokenUsage` once the stream ends — identical accounting to a non-streamed call. ## The monthly allotment The allotment is just one feature in the generic [Plans & Usage](https://docs.fastsvelte.dev/features/plans-and-usage/index.md) system — the `token_limit` `FeatureKey`, metered per billing period and resetting each period. Set each plan's token budget alongside its other limits; it isn't a separate AI-only mechanism. An org with no `token_limit` has a zero allotment and must rely on credit packs. ## Credit packs Packs are configured in **settings** — `settings.credit_packs`, with a default in `backend/app/config/settings.py` and overridable via the `FS_CREDIT_PACKS` env var (JSON). The defaults: | Pack | Tokens | Price | | ---------- | ---------- | ----- | | `pack_5m` | 5,000,000 | $39 | | `pack_10m` | 10,000,000 | $69 | | `pack_25m` | 25,000,000 | $149 | Change tokens or pricing in settings — no code edit, and **no Stripe products to create** (checkout builds the price inline via Stripe `price_data`). Purchased tokens are **non-expiring** and live in `organization_credit_balance`, separate from the per-period allotment. **Purchase flow** (`backend/app/api/route/credit_pack_route.py`, all `ORG_ADMIN`): | Endpoint | Purpose | | ------------------------------------- | ------------------------------------------------------------------------------------------ | | `GET /billing/credit-packs` | List available packs | | `POST /billing/credit-packs/checkout` | `{ pack_id, return_base_url }` → `{ url }` — redirect the user to this Stripe Checkout URL | The org must already have a Stripe customer (complete subscription billing setup first). Fulfillment is handled on the Stripe **`checkout.session.completed`** webhook (`backend/app/api/route/stripe_webhook_route.py`): `CreditPackService.fulfill_purchase` checks `metadata.type == "credit_pack"`, credits the balance, and appends an `organization_credit_transaction` audit row. ## Model pricing USD cost per call is computed from the `model_price` table (seeded by migrations `006_model_price.sql` + `008_more_model_prices.sql`), which ships current OpenAI GPT-4.1 and GPT-5 series pricing (standard tier, per 1M tokens). A sample: | Provider | Model | Input / 1M | Output / 1M | | -------- | ---------------------- | ---------- | ----------- | | openai | gpt-5-mini *(default)* | $0.25 | $2.00 | | openai | gpt-5 | $1.25 | $10.00 | | openai | gpt-5-nano | $0.05 | $0.40 | | openai | gpt-4.1-mini | $0.40 | $1.60 | | openai | gpt-4o-mini | $0.15 | $0.60 | …plus `gpt-5.1`, `gpt-5.2`, `gpt-4.1`, `gpt-4o`, and the `-pro` variants. `LlmPricingService.compute_cost_usd` looks up the price (cached) and multiplies by input/output tokens. **If you point the copilot at a model that isn't in this table, calls will fail** — add a row (or a migration) for any model you enable. ## Usage reporting `backend/app/api/route/usage_route.py` exposes: | Endpoint | Role | Returns | | ---------------------- | ---------------------------------------------- | ------------------------------------------------------------------------ | | `GET /usage/summary` | `MEMBER` | used / limit / credit tokens for the period, plus the caller's own usage | | `GET /usage/history` | `MEMBER` (own rows) · `ORG_ADMIN`+ (whole org) | paginated per-call log (`limit`, `offset`) | | `GET /usage/top-users` | `ORG_ADMIN` | top users by tokens this period | | `GET /usage/fleet` | `SYSTEM_ADMIN` | fleet-wide rollup over `days` — backs the admin AI-usage page | The user-facing usage page reads `/summary` + `/history`; the system-admin AI-usage page reads `/fleet`. ## Overage settings Overage is gated by two settings, **both `false` by default**, so usage beyond allotment + credits is blocked (a hard cap) rather than silently charged: - **`overage_enabled`** (org setting) — an org opts into overage. - **`ai_overage_enabled`** (system setting, `SYSTEM_ADMIN`) — an operator-level kill switch; an org's `overage_enabled` only takes effect if this is also on. Both use the existing generic setting mechanism (org settings via `/organization/{id}/{key}`, system settings via `/system/{key}`). Actually **charging** for overage (Stripe metered billing / invoice items) is intentionally not implemented yet — leave overage off until you wire it up. ## Schema Migration `007_ai_credit_billing.sql` adds: - `llm_usage_log` — append-only per-request log (tokens, cost, `bucket` = `allotment` | `credit` | `overage`). - `organization_credit_balance` — running non-expiring credit-pack balance. - `organization_credit_transaction` — audit log of credit purchases/debits. ## Next steps - **[AI Integration](https://docs.fastsvelte.dev/features/ai/index.md)** — the LLM client, the sample copilot, and streaming. - **[Stripe Integration](https://docs.fastsvelte.dev/features/billing/index.md)** — the subscription billing this builds on. # B2B Mode [FastSvelte](https://fastsvelte.dev) supports two operational modes: **B2C** (Business-to-Consumer) and **B2B** (Business-to-Business). This guide explains how B2B mode works. ## What is B2B Mode? In B2B mode, FastSvelte operates as a closed registration system where: - Public signup is disabled - Users can only join via invitation - Organizations are managed by system administrators - Each organization has its own admins who manage members ## Initial Setup When you run `uv run init.py` and select **B2B mode**, the script creates: 1. A system administrator account (sys_admin) 1. Sets `FS_MODE=b2b` in your environment ## The B2B Flow ### 1. System Administrator Login After running `init.py`, log in with the sys_admin credentials created during setup. ### 2. Create an Organization 1. Navigate to **System > Organizations** in the sidebar 1. Click **Create Organization** 1. Fill in the organization details: 1. **Organization Name**: The company/organization name 1. **Admin Email**: Email address for the organization administrator 1. **Admin Full Name**: Full name for billing purposes 1. Click **Create Organization** **What happens next:** - A new organization is created in the database - A Stripe customer is automatically created for billing - An invitation email is sent to the admin email address - The organization admin can accept the invitation to create their account ### 3. Organization Admin Accepts Invitation The organization admin receives an invitation email with a unique link. When they click it: 1. They're directed to the invitation acceptance page 1. They set their password and complete their profile 1. Their account is created with the `org_admin` role 1. They can now log in and access their organization Email Service in Development In development mode, the email service defaults to `stub` (no actual emails sent). To get invitation links, check your backend terminal for `[STUB EMAIL]` blocks containing the invitation URL. Copy and paste this URL in your browser to accept the invitation. ### 4. Organization Admin Manages Members Once logged in, the org_admin can: - **View Members**: See all users in their organization - **Invite Users**: Send invitations to new members - **Change Roles**: Update member roles (readonly, member, org_admin) - **Remove Members**: Remove users from the organization Access these features via **Organization > Users** in the sidebar. ### 5. Member Invitation Flow When an org_admin invites a new member: 1. Navigate to **Organization > Invitations** 1. Create a new invitation with email and role 1. The invited user receives an email with an invitation link 1. They accept the invitation and create their account 1. They automatically join the organization with the assigned role Finding Invitation Links in Development Since the email service defaults to `stub` in development, check your backend logs for `[STUB EMAIL]` blocks that contain the invitation URL. ## User Roles in B2B Mode | Role | Description | Permissions | | ----------- | -------------------------- | ---------------------------------------------------- | | `sys_admin` | System Administrator | Full system access, can manage all organizations | | `org_admin` | Organization Administrator | Can manage their organization's members and settings | | `member` | Regular Member | Can use application features | | `readonly` | Read-only User | View-only access | ## What's Disabled in B2B Mode When running in B2B mode: - ❌ Public signup page (`/signup`) returns 404 - ❌ OAuth signup creates accounts only for existing invited users - ❌ Individual billing is hidden (billing managed at organization level) - ✅ Only invitation-based registration is allowed ## Development vs Production The flow is the same in both development and production environments. The key differences: - **Development**: Uses local email preview (no actual emails sent by default) - **Production**: Sends real invitation emails via your configured email service ## Configuration B2B mode is controlled by the backend `FS_MODE` environment variable: ```bash # Backend (.env) FS_MODE=b2b ``` The frontend doesn't need a mode variable — it reads the mode from the backend's `/config` endpoint at runtime. Update `FS_MODE` and restart the backend. ## Common Workflows ### Adding a New Company 1. sys_admin creates organization 1. Organization admin accepts invitation 1. org_admin invites team members 1. Members accept invitations and join ### Managing Organization Members 1. org_admin views members at `/organization/users` 1. Change member roles with the "Change Role" button 1. Remove members with the "Remove" button 1. Cannot remove yourself or the last org_admin ### Suspending an Organization 1. sys_admin navigates to organization details 1. Clicks "Suspend Organization" 1. All users in the organization are blocked from logging in 1. Data is preserved and can be reactivated later # Database FastSvelte uses **PostgreSQL** with a multi-tenant schema and **Sqitch** for migrations. Repositories use raw SQL (no ORM), so you always see exactly what runs. ## Multi-tenant schema All business data is scoped to an **organization** (the tenant boundary). Core entities: - `user` — individual accounts - `organization` — the tenant; all business data belongs to one - `role` — `readonly`, `member`, `org_admin`, `sys_admin` (see [Authentication](https://docs.fastsvelte.dev/features/authentication/index.md)) - `session` — server-side sessions - `plan` / `organization_plan` — subscription tiers and the org's current plan This supports both individual users and teams without changing the data model. See [Multi-Tenancy (B2B)](https://docs.fastsvelte.dev/features/multi-tenancy/index.md) for the organization/invitation model. ## Connection config Connection settings come from `backend/.env` (`FS_DB_URL`, `FS_DB_SCHEMA`) via `backend/app/config/settings.py`, and are injected through the DI container's `db_config` into every repository — so repositories never construct their own connections. ## Migrations with Sqitch Migrations are plain SQL files in `backend/db/`, versioned with Sqitch and reviewed alongside code in pull requests. Create a migration: ```bash cd backend/db ./sqitch.sh add add_feature -n "Add feature table" ``` This creates three files: - `deploy/add_feature.sql` — how to apply the change - `revert/add_feature.sql` — how to undo it - `verify/add_feature.sql` — how to verify it worked Deploy: ```bash ./sqitch.sh dev deploy ``` The `sqitch.sh` wrapper runs Sqitch via the official Docker image (no local Sqitch install needed) and adds FastSvelte-specific safety: per-environment database URLs (dev/beta/gamma/prod/test), a check that every migration is wrapped in `BEGIN;`/`COMMIT;`, revert protection (requires `--to`), and `.env` loading per environment. ### Why Sqitch and raw SQL? Sqitch is language-agnostic and works with plain SQL — no ORM lock-in. You review and optimize the exact SQL that runs, and can still adopt SQLAlchemy or another tool later if your project needs it. See [Architecture](https://docs.fastsvelte.dev/reference/architecture/index.md) for the repository-layer reasoning. # Transactional Email FastSvelte sends transactional email — verification, password reset, and organization invitations — through a pluggable provider. Pick one of **Resend**, **SendGrid**, or **Azure Communication Services**, or use the **stub** provider in development. ## Choosing a Provider Set `FS_EMAIL_PROVIDER` in `backend/.env` to one of: `resend`, `sendgrid`, `azure`, or `stub`. For the providers you don't use, clean up all three places: 1. **`backend/pyproject.toml`** — delete the two package lines you don't need, then run `uv sync`: ```toml "azure-communication-email>=1.1.0", # FS_EMAIL_PROVIDER=azure "resend[async]>=2.30.1", # FS_EMAIL_PROVIDER=resend "sendgrid>=6.12.5", # FS_EMAIL_PROVIDER=sendgrid ``` ```bash cd backend && uv sync ``` 1. **`backend/.env`** — remove the env vars for the unused providers. 1. **`backend/app/config/settings.py`** — remove the corresponding settings fields (e.g. `resend_api_key`, `resend_sender_address`, `resend_sender_name`). ## Resend A modern email API with a generous free tier — recommended for most new projects. 1. Sign up at [resend.com](https://resend.com) and create an API key at [resend.com/api-keys](https://resend.com/api-keys). 1. Add and verify your sending domain at [resend.com/domains](https://resend.com/domains). 1. Configure `backend/.env`: ```bash FS_EMAIL_PROVIDER="resend" FS_RESEND_API_KEY="re_your-resend-api-key-here" FS_RESEND_SENDER_ADDRESS="noreply@yourdomain.com" FS_RESEND_SENDER_NAME="Your App Name" ``` ## SendGrid ```bash FS_EMAIL_PROVIDER="sendgrid" FS_SENDGRID_API_KEY="SG.your-sendgrid-api-key-here" FS_SENDGRID_SENDER_ADDRESS="noreply@yourdomain.com" FS_SENDGRID_SENDER_NAME="Your App Name" ``` Verify your sender email/domain in the SendGrid dashboard first. ## Azure Communication Services For Azure-based deployments: ```bash FS_EMAIL_PROVIDER="azure" FS_AZURE_EMAIL_CONNECTION_STRING="endpoint=https://..." FS_AZURE_EMAIL_SENDER_ADDRESS="noreply@yourdomain.com" ``` ## Development (Stub) For local development without sending real email: ```bash FS_EMAIL_PROVIDER="stub" ``` Emails are logged to the backend console instead of sent — look for `[STUB EMAIL]` blocks containing verification and invitation links. ## Testing ```bash curl -X POST http://localhost:8000/password/forgot \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com"}' ``` ## Troubleshooting - **Not sending** — verify the API key, confirm the sender domain/address is verified with the provider, and check the backend logs. - **Production** — set up SPF/DKIM for your sending domain. # Landing Page Template FastSvelte includes a standalone, conversion-focused **landing site** in `landing/` — a separate SvelteKit app from the dashboard (`frontend/`), so your marketing pages deploy independently of the app. It's themed (light/dark) and SEO-optimized out of the box. The steps below take you from the template to your own landing page. ## 1. Find it The landing site lives in `landing/`. The home page (`landing/src/routes/+page.svelte`) is assembled from section components in `landing/src/lib/components/landing/`. Edit a section to change its content; reorder or remove sections in `+page.svelte`. ## 2. Set your branding - `landing/src/lib/config.ts` — `appName` and `siteUrl`. - `Logo.svelte` — swap in your logo (light/dark variants). - `landing/.env` — browser-exposed vars use the `PUBLIC_*` prefix (e.g. `PUBLIC_APP_NAME`, `PUBLIC_API_BASE_URL`). Run `npx svelte-kit sync` after changing them. ## 3. Arrange your sections `+page.svelte` composes the page from these sections, in order: ```text