# Admin API: platform stats and user management Source: https://docs.shipfastai.dev/api-reference/admin Reference for admin-only endpoints: aggregate platform statistics and paginated user listing. Requires superuser access. The Admin API provides platform-level visibility into user metrics and account management. All endpoints require superuser authentication and are mounted under `/api/admin/`. These endpoints are restricted to superuser accounts. Regular users receive a `403 Forbidden` response. *** ## GET /api/admin/stats Return aggregate statistics for the admin dashboard. **Headers:** `Bearer ` — must be a superuser token. ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/admin/stats \ --header "Authorization: Bearer " ``` **Response:** Total number of registered user accounts. Number of users with `subscription_status` set to `active`. Number of users who registered in the last 7 days. Number of users who have verified their email address. ```json theme={null} { "total_users": 1250, "active_subscriptions": 340, "new_users_7d": 42, "verified_users": 980 } ``` *** ## GET /api/admin/users Return a paginated list of all user accounts, ordered by creation date (newest first). **Headers:** `Bearer ` — must be a superuser token. **Query parameters:** Page number. Must be 1 or greater. Number of users per page. Must be between 1 and 100. ```bash curl theme={null} curl --request GET \ --url "http://localhost:8000/api/admin/users?page=1&limit=20" \ --header "Authorization: Bearer " ``` ```python Python theme={null} import requests response = requests.get( "http://localhost:8000/api/admin/users", headers={"Authorization": f"Bearer {access_token}"}, params={"page": 1, "limit": 20}, ) data = response.json() print(f"Showing {len(data['users'])} of {data['total']} users") ``` **Response:** Array of user objects for the requested page. UUID of the user. User's email address. User's display name. Whether the account is active. Whether the email is verified. Current subscription status. Current subscription tier. ISO 8601 account creation timestamp. ISO 8601 timestamp of the last login, or `null`. Total number of users across all pages. Current page number. Number of users per page. Total number of pages. ```json theme={null} { "users": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "is_active": true, "is_verified": true, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2026-04-01T10:30:00Z", "last_login_at": "2026-04-09T09:00:00Z" } ], "total": 1250, "page": 1, "limit": 20, "pages": 63 } ``` # AI chat and completions API endpoints Source: https://docs.shipfastai.dev/api-reference/ai-chat POST /api/ai/chat and POST /api/ai/completions — multi-turn chat, streaming SSE responses, and one-shot completions via OpenAI, Anthropic, or Gemini. The AI Chat API provides two endpoints for interacting with large language models: a multi-turn chat endpoint that supports streaming, and a single-prompt completions endpoint. Both are available on the **Pro** and **Enterprise** tiers and are subject to rate limiting. All endpoints are mounted under `/api/ai/`. These endpoints are available on Pro and Enterprise plans only. Requests from free-tier users will be rejected with a `403` response. *** ## POST /api/ai/chat Send a conversation to the configured LLM provider and receive a response. You can choose the provider (`openai`, `anthropic`, or `gemini`) and optionally stream the response as server-sent events. **Headers:** `Bearer ` **Request body:** An ordered list of messages representing the conversation history. Each message must have a `role` and `content`. The speaker role. One of `"system"`, `"user"`, or `"assistant"`. The text content of the message. The LLM provider to use. One of `"openai"`, `"anthropic"`, or `"gemini"`. The provider must be configured with a valid API key in your backend environment. The specific model to use (e.g., `"gpt-4o"`, `"claude-3-5-sonnet-20241022"`, `"gemini-1.5-pro"`). If omitted, the provider's default model is used. Sampling temperature between `0.0` and `2.0`. Lower values produce more deterministic output; higher values increase creativity. Maximum number of tokens to generate. Must be between `1` and `16384`. When `true`, the response is streamed as server-sent events (SSE). Each event contains a `token` field with the next piece of text. The stream ends with `data: [DONE]`. ### Non-streaming example ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/ai/chat \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain the difference between async and sync Python in one sentence."} ], "provider": "openai", "model": "gpt-4o", "temperature": 0.5, "max_tokens": 200 }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/ai/chat", headers={"Authorization": f"Bearer {access_token}"}, json={ "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain the difference between async and sync Python in one sentence."}, ], "provider": "openai", "model": "gpt-4o", "temperature": 0.5, "max_tokens": 200, }, ) print(response.json()["content"]) ``` **Response** — `ChatResponse`: The full generated text response from the model. The model identifier that was used to generate the response. Token consumption breakdown for the request. Number of tokens in the input messages. Number of tokens in the generated response. Total tokens consumed by the request. ```json theme={null} { "content": "Synchronous Python executes code line by line and blocks until each operation completes, while asynchronous Python uses `async`/`await` to pause and resume coroutines, allowing other tasks to run during waiting periods.", "model": "gpt-4o-2024-08-06", "usage": { "prompt_tokens": 38, "completion_tokens": 42, "total_tokens": 80 } } ``` ### Streaming example Set `"stream": true` to receive the response token by token as server-sent events. Each event is a JSON object with a `token` field. The final event is the literal string `[DONE]`. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/ai/chat \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --no-buffer \ --data '{ "messages": [{"role": "user", "content": "Count to five."}], "stream": true }' ``` ```python Python theme={null} import requests import json with requests.post( "http://localhost:8000/api/ai/chat", headers={"Authorization": f"Bearer {access_token}"}, json={"messages": [{"role": "user", "content": "Count to five."}], "stream": True}, stream=True, ) as response: for line in response.iter_lines(): if line: raw = line.decode("utf-8") if raw.startswith("data: "): payload = raw[6:] if payload == "[DONE]": break data = json.loads(payload) print(data["token"], end="", flush=True) ``` **SSE stream format:** ``` data: {"token": "One"} data: {"token": ","} data: {"token": " two"} data: {"token": ", three, four, five."} data: [DONE] ``` *** ## POST /api/ai/completions Generate a single completion from a plain text prompt, without a conversation history. Useful for summarization, classification, code generation, and other single-turn tasks. **Headers:** `Bearer ` **Request body:** The user's input prompt. An optional system message that sets the model's behavior for this request (e.g., `"You are a JSON formatter."`). The LLM provider to use. One of `"openai"`, `"anthropic"`, or `"gemini"`. The specific model to use. If omitted, the provider's default model is used. Sampling temperature between `0.0` and `2.0`. Maximum number of tokens to generate. Must be between `1` and `16384`. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/ai/completions \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "prompt": "Summarize the following in one sentence: FastAPI is a modern, fast web framework for building APIs with Python 3.7+ based on standard Python type hints.", "system_prompt": "You are a concise technical writer.", "provider": "openai", "max_tokens": 100 }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/ai/completions", headers={"Authorization": f"Bearer {access_token}"}, json={ "prompt": "Summarize the following in one sentence: FastAPI is a modern, fast web framework...", "system_prompt": "You are a concise technical writer.", "provider": "openai", "max_tokens": 100, }, ) print(response.json()["completion"]) ``` **Response:** The generated text response. The model identifier that produced the response. Token consumption breakdown (same shape as `ChatResponse.usage`). ```json theme={null} { "completion": "FastAPI is a high-performance Python web framework for building APIs using standard type hints.", "model": "gpt-4o-2024-08-06", "usage": { "prompt_tokens": 52, "completion_tokens": 17, "total_tokens": 69 } } ``` # API keys: create, list, revoke, and update Source: https://docs.shipfastai.dev/api-reference/api-keys Reference for the API Key management endpoints. Create keys for programmatic access, list existing keys, update names, and revoke keys. The API Keys endpoints let you manage programmatic access tokens for the Shipfastai API. API keys can be used as an alternative to JWT Bearer tokens for authenticating requests to AI and RAG endpoints. All key management operations require JWT authentication — you cannot use an API key to manage other API keys. API key management is available on **Pro** and **Enterprise** plans only. All endpoints are mounted under `/api/api-keys/`. *** ## POST /api/api-keys Create a new API key. The plaintext key is returned **only once** in the response. Store it securely — it cannot be retrieved again after creation. **Headers:** `Bearer ` — JWT authentication only. **Request body:** A descriptive name for the API key (e.g., `"Production server"`, `"CI pipeline"`). Optional ISO 8601 expiration timestamp. If omitted, the key does not expire. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/api-keys \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "name": "Production server", "expires_at": "2027-01-01T00:00:00Z" }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/api-keys", headers={"Authorization": f"Bearer {access_token}"}, json={ "name": "Production server", "expires_at": "2027-01-01T00:00:00Z", }, ) data = response.json() print(f"Save this key: {data['key']}") ``` **Response** (`201 Created`) — `ApiKeyCreated`: UUID of the API key. The name you assigned to the key. The first 12 characters of the key, used for identification in listings. The full plaintext API key. **This is the only time the full key is returned.** Store it securely. Whether the key is active. `true` on creation. ISO 8601 timestamp of when the key was created. ISO 8601 timestamp of last usage. `null` for a newly created key. ISO 8601 expiration timestamp, or `null` if the key does not expire. ```json theme={null} { "id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "name": "Production server", "key_prefix": "sk_a1b2c3d4e5", "key": "sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", "is_active": true, "created_at": "2026-04-09T14:30:00Z", "last_used_at": null, "expires_at": "2027-01-01T00:00:00Z" } ``` *** ## GET /api/api-keys List all API keys for the authenticated user, ordered by creation date (newest first). The full key value is never included — only the prefix is shown for identification. **Headers:** `Bearer ` — JWT authentication only. ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/api-keys \ --header "Authorization: Bearer " ``` **Response** — array of `ApiKeyResponse`: UUID of the API key. The name assigned to the key. The first 12 characters of the key. Whether the key is active. Revoked keys have `is_active: false`. ISO 8601 creation timestamp. ISO 8601 timestamp of last usage, or `null`. ISO 8601 expiration timestamp, or `null`. ```json theme={null} [ { "id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "name": "Production server", "key_prefix": "sk_a1b2c3d4e5", "is_active": true, "created_at": "2026-04-09T14:30:00Z", "last_used_at": "2026-04-09T15:00:00Z", "expires_at": "2027-01-01T00:00:00Z" } ] ``` *** ## DELETE /api/api-keys/ Revoke an API key. This is a soft delete — the key is marked as inactive and can no longer be used for authentication. The key record is retained for audit purposes. **Path parameters:** The UUID of the API key to revoke. **Headers:** `Bearer ` — JWT authentication only. ```bash theme={null} curl --request DELETE \ --url http://localhost:8000/api/api-keys/9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d \ --header "Authorization: Bearer " ``` **Response:** ```json theme={null} { "message": "API key revoked" } ``` Returns `404` if the key does not exist or does not belong to the authenticated user. *** ## PATCH /api/api-keys/ Update an API key's display name. **Path parameters:** The UUID of the API key to update. **Headers:** `Bearer ` — JWT authentication only. **Query parameters:** The new name for the API key. ```bash theme={null} curl --request PATCH \ --url "http://localhost:8000/api/api-keys/9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d?name=Staging%20server" \ --header "Authorization: Bearer " ``` **Response** — `ApiKeyResponse` with updated fields: ```json theme={null} { "id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "name": "Staging server", "key_prefix": "sk_a1b2c3d4e5", "is_active": true, "created_at": "2026-04-09T14:30:00Z", "last_used_at": "2026-04-09T15:00:00Z", "expires_at": "2027-01-01T00:00:00Z" } ``` Returns `404` if the key does not exist or does not belong to the authenticated user. *** ## Using API keys for authentication Once you have a key, pass it in the `Authorization` header as a Bearer token, the same way you pass a JWT: ```http theme={null} Authorization: Bearer sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 ``` API keys are accepted on AI and RAG endpoints alongside JWTs. The backend automatically detects whether the token is a JWT or an API key. # Authentication endpoints: register, login, OAuth Source: https://docs.shipfastai.dev/api-reference/auth Full reference for all Shipfastai authentication endpoints including registration, login, token refresh, email verification, password reset, and OAuth. The auth API handles the complete identity lifecycle: account creation, credential-based login, JWT token management, email verification, password recovery, and OAuth sign-in via Google or GitHub. All endpoints are mounted under the `/api/auth/` prefix. *** ## POST /api/auth/register Create a new user account. After registration, a verification email is sent to the provided address. The user's email address. Must be a valid email format and not already registered. The user's password in plain text. It is hashed before storage. The user's display name. Optional. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/auth/register \ --header "Content-Type: application/json" \ --data '{ "email": "user@example.com", "password": "secure-password-123", "full_name": "Jane Smith" }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/auth/register", json={ "email": "user@example.com", "password": "secure-password-123", "full_name": "Jane Smith", }, ) print(response.json()) ``` **Response** — `UserResponse`: UUID of the newly created user. The registered email address. The user's display name, if provided. Profile picture URL. `null` for newly registered users. Whether the account is active. `true` by default on registration. Whether the email has been verified. `false` until the verification link is clicked. The OAuth provider used to sign in (`google`, `github`), or `null` for password-based accounts. Current subscription status (e.g., `free`, `active`, `cancelled`). Subscription tier (e.g., `free`, `pro`). ISO 8601 timestamp of when the account was created. ```json theme={null} { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "avatar_url": null, "is_active": true, "is_verified": false, "oauth_provider": null, "subscription_status": "free", "subscription_tier": "free", "created_at": "2024-01-15T10:30:00Z" } ``` *** ## POST /api/auth/login Authenticate with email and password. Returns a JWT access token and refresh token. The user's registered email address. The user's password. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/auth/login \ --header "Content-Type: application/json" \ --data '{ "email": "user@example.com", "password": "secure-password-123" }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/auth/login", json={"email": "user@example.com", "password": "secure-password-123"}, ) data = response.json() ``` **Response** — `Token`: JWT access token. Valid for 30 minutes. Pass this in the `Authorization` header. JWT refresh token. Use this to obtain a new access token after expiry. Always `"bearer"`. The authenticated user object. See `UserResponse` fields above. UUID of the user. User's email address. User's display name. Email verification status. Current subscription status. Current subscription tier. Account creation timestamp. ```json theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", "user": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "is_verified": true, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2024-01-15T10:30:00Z" } } ``` *** ## POST /api/auth/refresh Exchange a valid refresh token for a new access token and refresh token pair. Both tokens are rotated on every call. A valid, unexpired refresh token previously issued by `/api/auth/login` or a prior `/api/auth/refresh` call. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/refresh \ --header "Content-Type: application/json" \ --data '{"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}' ``` **Response** — `Token` (same shape as login, `user` field is `null`): ```json theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", "user": null } ``` *** ## POST /api/auth/logout Log out the current user. Because JWTs are stateless, the server does not invalidate the token — the client is responsible for discarding both tokens from storage. No request body required. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/logout ``` **Response:** ```json theme={null} { "message": "Successfully logged out" } ``` *** ## GET /api/auth/me Return the profile of the currently authenticated user. **Headers:** `Bearer ` ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/auth/me \ --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` **Response** — `UserResponse` (see fields above): ```json theme={null} { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "avatar_url": null, "is_active": true, "is_verified": true, "oauth_provider": null, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2024-01-15T10:30:00Z" } ``` *** ## POST /api/auth/verify-email Verify a user's email address using the token sent to them after registration. The verification token extracted from the link in the verification email. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/verify-email \ --header "Content-Type: application/json" \ --data '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}' ``` **Response:** ```json theme={null} { "message": "Email verified successfully" } ``` If the email was already verified, the response is: ```json theme={null} { "message": "Email already verified" } ``` *** ## POST /api/auth/resend-verification Resend the verification email to a registered address. This endpoint always returns `200` regardless of whether the email is registered, to prevent email enumeration. The email address to resend the verification link to. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/resend-verification \ --header "Content-Type: application/json" \ --data '{"email": "user@example.com"}' ``` **Response:** ```json theme={null} { "message": "If your email is registered, a verification link has been sent" } ``` *** ## POST /api/auth/forgot-password Request a password reset email. Like resend-verification, this always returns `200` to prevent email enumeration. The email address associated with the account. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/forgot-password \ --header "Content-Type: application/json" \ --data '{"email": "user@example.com"}' ``` **Response:** ```json theme={null} { "message": "If your email is registered, a password reset link has been sent" } ``` *** ## POST /api/auth/verify-reset-token Check whether a password reset token is still valid before presenting the reset form to the user. The password reset token from the reset email link. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/verify-reset-token \ --header "Content-Type: application/json" \ --data '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}' ``` **Response:** ```json theme={null} { "valid": true } ``` Returns `400` with `{"detail": "Invalid or expired reset token"}` if the token is invalid or expired. *** ## POST /api/auth/reset-password Set a new password using a valid password reset token. The password reset token from the reset email link. The new password to set for the account. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/reset-password \ --header "Content-Type: application/json" \ --data '{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "new_password": "new-secure-password-456" }' ``` **Response:** ```json theme={null} { "message": "Password reset successfully" } ``` *** ## GET /api/auth/oauth/ Initiate an OAuth sign-in flow. Returns the authorization URL that you redirect the user to. Supported providers are `google` and `github`. The OAuth provider to use. Must be `google` or `github`. The provider must be configured in your backend settings. ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/auth/oauth/google ``` **Response:** The full URL to redirect the user to in order to begin the OAuth flow. A CSRF state token. Pass this back when handling the OAuth callback to validate the flow. ```json theme={null} { "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&state=...", "state": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` After the user authorizes the app, the provider redirects to `GET /api/auth/callback/{provider}`, which exchanges the code for tokens and redirects the browser to `{FRONTEND_URL}/auth/callback?access_token=...&refresh_token=...`. The callback endpoint is handled automatically by the backend — you do not need to call it directly. Configure your OAuth app's redirect URI to point to `{your-backend-url}/api/auth/callback/{provider}`. # Authenticating API requests with Bearer tokens Source: https://docs.shipfastai.dev/api-reference/authentication Learn how to obtain a Bearer token via POST /api/auth/login, pass it in the Authorization header, and refresh it when it expires. All protected endpoints in the Shipfastai API require a Bearer token passed in the `Authorization` request header. You obtain a token by logging in, and you keep access alive by refreshing it before it expires. This page walks you through the full token lifecycle. ## Obtaining a token Call `POST /api/auth/login` with your email and password. On success, the response includes an `access_token` and a `refresh_token`. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/auth/login \ --header "Content-Type: application/json" \ --data '{ "email": "user@example.com", "password": "your-password" }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/auth/login", json={ "email": "user@example.com", "password": "your-password", }, ) data = response.json() access_token = data["access_token"] refresh_token = data["refresh_token"] ``` **Response:** ```json theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", "user": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "is_verified": true, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2024-01-15T10:30:00Z" } } ``` ## Passing the token Include the access token in the `Authorization` header of every request to a protected endpoint: ```http theme={null} Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` ```bash curl theme={null} curl --request GET \ --url http://localhost:8000/api/auth/me \ --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ```python Python theme={null} import requests headers = {"Authorization": f"Bearer {access_token}"} response = requests.get( "http://localhost:8000/api/auth/me", headers=headers, ) print(response.json()) ``` ## Token expiry and refresh Access tokens expire after **30 minutes**. Before making a request after expiry, call `POST /api/auth/refresh` with your refresh token to receive a new token pair. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/auth/refresh \ --header "Content-Type: application/json" \ --data '{"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}' ``` The response has the same shape as the login response: a new `access_token` and a new `refresh_token`. Replace both stored tokens with the new values. Store your refresh token in a secure, persistent location such as an `httpOnly` cookie or an encrypted local store. Never expose it in JavaScript accessible to the page or in localStorage without additional protections, as a stolen refresh token grants long-lived access. ## Error responses ### 401 Unauthorized Returned when the token is missing, malformed, or expired. ```json theme={null} { "detail": "Could not validate credentials" } ``` ### 403 Forbidden Returned when the token is valid but the account is inactive. ```json theme={null} { "detail": "Account is inactive" } ``` When you receive a `401`, attempt to refresh the access token. If the refresh also fails with a `401`, the user's session has expired and they must log in again. # Billing API: Stripe checkout and subscriptions Source: https://docs.shipfastai.dev/api-reference/billing Reference for Stripe-powered billing endpoints: create a checkout session, open the customer portal, query subscription info, list plans, and handle lifecycle webhooks. The Billing API integrates with Stripe to handle subscription checkout, customer portal access, subscription queries, and lifecycle events. All billing endpoints are mounted under `/api/billing/`. Protected endpoints require a valid Bearer token for the authenticated user. You must configure `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` in your backend environment variables before these endpoints will work. See your backend `.env` for the required keys. *** ## POST /api/billing/create-checkout-session Create a Stripe Checkout session for a subscription. If the authenticated user does not yet have a Stripe customer record, one is created automatically using their name and email. Returns the Stripe-hosted checkout URL to redirect the user to. **Headers:** `Bearer ` **Query parameters:** The Stripe Price ID for the subscription plan (e.g., `price_1OqXxxxxxYYYYYYYY`). You can find price IDs in the Stripe Dashboard or via the Stripe CLI. ```bash curl theme={null} curl --request POST \ --url "http://localhost:8000/api/billing/create-checkout-session?price_id=price_1OqXxxxxxYYYYYYYY" \ --header "Authorization: Bearer " ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/billing/create-checkout-session", headers={"Authorization": f"Bearer {access_token}"}, params={"price_id": "price_1OqXxxxxxYYYYYYYY"}, ) checkout_url = response.json()["url"] # Redirect the user to checkout_url ``` **Response:** The Stripe-hosted checkout URL. Redirect the user's browser to this URL to complete payment. ```json theme={null} { "url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4..." } ``` After a successful payment, Stripe redirects to `{FRONTEND_URL}/dashboard?success=true`. If the user cancels, Stripe redirects to `{FRONTEND_URL}/pricing?canceled=true`. *** ## POST /api/billing/create-portal-session Create a Stripe Customer Portal session for the authenticated user. The portal lets the user manage their subscription, update payment methods, view invoices, and cancel. The user must have an existing Stripe customer record (created automatically at checkout). **Headers:** `Bearer ` No request body is required. The endpoint uses the `stripe_customer_id` stored on the authenticated user. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/billing/create-portal-session \ --header "Authorization: Bearer " ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/billing/create-portal-session", headers={"Authorization": f"Bearer {access_token}"}, ) portal_url = response.json()["url"] # Redirect the user to portal_url ``` **Response:** The Stripe-hosted billing portal URL. Redirect the user's browser to this URL. ```json theme={null} { "url": "https://billing.stripe.com/session/bps_test_a1b2c3d4..." } ``` After the user leaves the portal, Stripe redirects them to `{FRONTEND_URL}/dashboard`. Returns `400` with `{"detail": "No billing account found"}` if the user has never completed a checkout session and therefore has no Stripe customer ID. *** ## GET /api/billing/subscription Return the current subscription status for the authenticated user. **Headers:** `Bearer ` ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/billing/subscription \ --header "Authorization: Bearer " ``` **Response:** Current subscription status (e.g., `free`, `active`, `cancelled`, `past_due`). Current subscription tier (e.g., `free`, `pro`). The Stripe customer ID associated with the user, or `null` if no Stripe record exists. ```json theme={null} { "status": "active", "tier": "pro", "stripe_customer_id": "cus_a1b2c3d4e5f6" } ``` *** ## GET /api/billing/plans Return the list of available subscription plans with their features. This endpoint does not require authentication. ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/billing/plans ``` **Response:** Array of available subscription plans. Display name of the plan. Tier identifier (e.g., `free`, `pro`). Fixed price for free-tier plans (`0`). Stripe Price ID for monthly billing. Present on paid tiers. Stripe Price ID for yearly billing. Present on paid tiers. List of features included in the plan. ```json theme={null} { "plans": [ { "name": "Free", "tier": "free", "price": 0, "features": [ "Basic authentication", "User management", "Community support" ] }, { "name": "Pro", "tier": "pro", "price_monthly": "price_1OqXxxxxxMONTHLY", "price_yearly": "price_1OqXxxxxxYEARLY", "features": [ "Everything in Free", "AI Chat (OpenAI, Anthropic, Gemini)", "RAG Pipeline", "API Key access", "Priority support" ] } ] } ``` *** ## POST /api/billing/webhook Stripe webhook endpoint. This endpoint is called directly by Stripe — not by your application. Stripe sends signed events here to notify your backend of subscription changes. Do not call this endpoint from your application. Register it in the Stripe Dashboard (or via the Stripe CLI) as your webhook URL: `https:///api/billing/webhook`. **Headers required by Stripe:** The `Stripe-Signature` header added automatically by Stripe. The backend verifies this signature against your `STRIPE_WEBHOOK_SECRET` to confirm the event is authentic. ### Handled events | Event type | Effect | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `checkout.session.completed` | Sets `subscription_status` to `active` and determines `subscription_tier` from the checkout line items using the configured price-to-tier mapping. | | `customer.subscription.updated` | Updates `subscription_status` on the matching user to the new Stripe subscription status. | | `customer.subscription.deleted` | Sets `subscription_status` to `cancelled` and `subscription_tier` to `free` for the customer. | | `invoice.payment_failed` | Sets `subscription_status` to `past_due` for the customer. | **Response** (on successful receipt): ```json theme={null} { "received": true } ``` Returns `400` for an invalid payload or a failed signature verification. ### Local testing with the Stripe CLI To test webhooks locally, forward events from Stripe to your running dev server: ```bash theme={null} stripe listen --forward-to http://localhost:8000/api/billing/webhook ``` The CLI prints a webhook signing secret that you should set as `STRIPE_WEBHOOK_SECRET` in your local `.env` while testing. # Checkout and download API Source: https://docs.shipfastai.dev/api-reference/checkout-download Reference for the storefront checkout flow, product download with license-key-encrypted ZIP delivery, and download history. The storefront API handles one-time product purchases through Stripe Checkout and delivers the purchased product as a password-protected ZIP file. These endpoints power the main Shipfastai storefront and are separate from the subscription billing endpoints used inside each product tier. Checkout and download endpoints use Supabase authentication (cookie-based). Make sure you are logged in through the Shipfastai frontend before calling these endpoints. *** ## GET /api/checkout Start a one-time purchase flow for a product tier. This endpoint redirects the browser to a Stripe-hosted checkout page. If the user is not authenticated, they are redirected to the login page first. If the user already owns the requested tier, they are redirected to the dashboard. **Query parameters:** The product tier to purchase. Must be one of `basic`, `pro`, or `enterprise`. ```bash theme={null} curl --request GET \ --url "https://your-app.com/api/checkout?tier=pro" \ --cookie "session=..." ``` **Behavior:** | Condition | Result | | ----------------------------- | -------------------------------------------------------- | | Valid tier, authenticated | Redirects to Stripe Checkout page | | Valid tier, not authenticated | Redirects to `/login?redirect=/api/checkout?tier={tier}` | | User already owns the tier | Redirects to `/dashboard` | | Invalid or missing tier | Returns `400` with `{"error": "Invalid tier specified"}` | After successful payment, Stripe redirects to `/dashboard?success=true&session_id={id}`. If the user cancels, Stripe redirects to `/pricing?canceled=true`. ### Available tiers | Tier | Price | Includes | | ------------ | ----- | ------------------------------------------------------------------------------------ | | `basic` | \$199 | FastAPI + Next.js boilerplate, JWT auth, OAuth, Stripe subscriptions, Docker, CI/CD | | `pro` | \$299 | Everything in Basic plus RAG pipeline, streaming LLM chat, admin dashboard, API keys | | `enterprise` | \$499 | Everything in Pro plus fine-tuning scripts, multi-tenancy, usage-based billing | Promotion codes are supported. Stripe Checkout displays a promotion code field automatically. *** ## GET /api/download Download the purchased product as an AES-256 encrypted ZIP file. The ZIP password is the license key issued at purchase time. The license key is also returned in the `X-License-Key` response header. **Query parameters:** The UUID of a specific purchase to download. If omitted, the most recent completed purchase is used. ```bash theme={null} curl --request GET \ --url "https://your-app.com/api/download?purchase_id=abc123" \ --cookie "session=..." \ --output product.zip ``` **Response headers:** | Header | Description | | --------------------- | --------------------------------------------------------------- | | `Content-Type` | `application/zip` | | `Content-Disposition` | `attachment; filename="Shipfastai-{tier}-{license_prefix}.zip"` | | `X-License-Key` | The full license key used as the ZIP password | ### Rate limiting Each user is limited to one download per purchase every **60 seconds**. Requests within the cooldown window return `429`: ```json theme={null} { "error": "Please wait 45 seconds before downloading again." } ``` ### Download limits Each purchase has a maximum number of downloads (default: **5** for new purchases). Once the limit is reached, the purchase status changes to `expired` and further downloads are blocked. ```json theme={null} { "error": "Download limit reached. Your license has expired. Please purchase a new license to continue downloading.", "expired": true } ``` ### Error responses | Status | Condition | | ------ | --------------------------------------------------- | | `401` | Not authenticated | | `403` | No active purchase found, or download limit reached | | `404` | Product files not found for the given tier | | `429` | Rate limit — download requested too soon | | `500` | ZIP creation failed or unexpected error | *** ## GET /api/download/history Retrieve the download history for the authenticated user. Returns the most recent 50 download log entries, ordered by download time (newest first). **Query parameters:** Filter results to a specific purchase. If omitted, returns download logs across all purchases. ```bash theme={null} curl --request GET \ --url "https://your-app.com/api/download/history?purchase_id=abc123" \ --cookie "session=..." ``` **Response** — array of download log entries: UUID of the download log entry. UUID of the associated purchase. UUID of the user who performed the download. The product version that was downloaded (e.g., `"1.0.0"`). IP address of the client at the time of download. User-Agent header of the client at the time of download. Country code derived from the client IP address, if available. ISO 8601 timestamp of when the download occurred. ```json theme={null} [ { "id": "d4e5f6a1-b2c3-4d5e-f6a1-b2c3d4e5f6a1", "purchase_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "version": "1.0.0", "ip_address": "203.0.113.42", "user_agent": "Mozilla/5.0 ...", "country": "US", "downloaded_at": "2026-04-09T14:30:00Z" } ] ``` *** ## POST /api/stripe/webhook Stripe webhook endpoint for the storefront. Handles purchase completion, payment confirmation, and refund events. This endpoint is called directly by Stripe, not by your application. Do not call this endpoint from your application. Register it in the Stripe Dashboard as your webhook URL: `https://your-app.com/api/stripe/webhook`. **Headers required by Stripe:** The `Stripe-Signature` header added automatically by Stripe. The backend verifies this against `STRIPE_WEBHOOK_SECRET`. ### Handled events | Event type | Effect | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `checkout.session.completed` | Creates a purchase record with a generated license key, 5 max downloads, and 12-month update entitlement (`entitled_until`). Sends a purchase confirmation email. | | `payment_intent.succeeded` | Same as `checkout.session.completed`. Falls back to looking up the checkout session if metadata is not on the payment intent directly. | | `charge.refunded` | Sets the purchase status to `refunded` for the matching payment. | **Response:** ```json theme={null} { "received": true } ``` Returns `400` for an invalid payload or failed signature verification. ### Local testing with the Stripe CLI Forward events from Stripe to your local development server: ```bash theme={null} stripe listen --forward-to http://localhost:3000/api/stripe/webhook ``` Set the webhook signing secret printed by the CLI as `STRIPE_WEBHOOK_SECRET` in your `.env.local`. # Contact API Source: https://docs.shipfastai.dev/api-reference/contact Reference for the contact form endpoint that sends support messages and confirmation emails. The contact API accepts form submissions from the Shipfastai website, sends a notification email to the support team, and sends a confirmation email to the submitter. *** ## POST /api/contact Submit a contact form message. The endpoint sends a styled notification email to the support team and a confirmation email to the submitter. **Request body:** Full name of the person submitting the form. Email address for the reply and confirmation email. The subject category. One of `sales`, `support`, `enterprise`, `partnership`, or `other`. The message body. ```bash theme={null} curl --request POST \ --url "https://your-app.com/api/contact" \ --header "Content-Type: application/json" \ --data '{ "name": "Jane Smith", "email": "jane@example.com", "subject": "support", "message": "I need help with my license key." }' ``` ### Subject categories | Value | Label | | ------------- | -------------------- | | `sales` | Sales Inquiry | | `support` | Technical Support | | `enterprise` | Enterprise Solutions | | `partnership` | Partnership | | `other` | General Inquiry | ### Success response ```json theme={null} { "success": true } ``` ### Error responses | Status | Condition | | ------ | ----------------------------------------------------------------- | | `400` | Missing required field (`name`, `email`, `subject`, or `message`) | | `500` | Failed to send the notification email | # Shipfastai REST API overview and base URLs Source: https://docs.shipfastai.dev/api-reference/overview Overview of the Shipfastai REST API: base URLs, how to pass your Bearer token, response format, rate limits, and the full list of HTTP error codes. The Shipfastai REST API gives you programmatic access to authentication, user management, billing, AI chat, retrieval-augmented generation (RAG), product checkout and download, API key management, and admin features. Every feature exposed in the product is backed by an HTTP endpoint, so you can integrate it into your own frontend or automate workflows from any HTTP client. ## Endpoint groups | Group | Prefix | Description | | ------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------ | | [Auth](/api-reference/auth) | `/api/auth/` | Registration, login, token refresh, email verification, password reset, OAuth | | [Users](/api-reference/users) | `/api/users/` | Profile management, password and email changes, data export, account deletion | | [Billing](/api-reference/billing) | `/api/billing/` | Stripe subscription checkout, customer portal, subscription status, plan listing, webhooks | | [Checkout & Download](/api-reference/checkout-download) | `/api/checkout`, `/api/download` | One-time product purchases and encrypted ZIP downloads | | [AI Chat](/api-reference/ai-chat) | `/api/ai/` | Multi-turn chat and single-prompt completions via OpenAI, Anthropic, or Gemini | | [RAG](/api-reference/rag) | `/api/rag/` | Document ingestion, semantic search, and retrieval-augmented generation queries | | [API Keys](/api-reference/api-keys) | `/api/api-keys/` | Create, list, revoke, and update API keys for programmatic access | | [Contact](/api-reference/contact) | `/api/contact` | Contact form submission with email notifications | | [Admin](/api-reference/admin) | `/api/admin/` | Platform statistics and user management (superuser only) | ## Base URL | Environment | Base URL | | ----------- | ------------------------------------- | | Development | `http://localhost:8000` | | Production | `https://` | All endpoints are prefixed with `/api/`. For example, the login endpoint is available at `http://localhost:8000/api/auth/login`. ## Interactive docs When running in development mode, the API server exposes two auto-generated documentation UIs: * **Swagger UI** — `http://localhost:8000/docs` * **ReDoc** — `http://localhost:8000/redoc` Both UIs are generated directly from the live OpenAPI schema and let you make real requests against your local server. The interactive docs are disabled in production by default (`DEBUG=false`). To enable them in a deployed environment, set `DEBUG=true` — but avoid doing this on public-facing production instances. ## Authentication All protected endpoints require a Bearer token in the `Authorization` header. You obtain a token by calling `POST /api/auth/login`. ```http theme={null} Authorization: Bearer ``` See the [Authentication guide](/api-reference/authentication) for full details on obtaining and refreshing tokens. ## Response format Every response body is JSON. Successful responses return the resource or a confirmation message directly as the top-level object — there is no shared envelope wrapper. ```json theme={null} { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "is_verified": true, "created_at": "2024-01-15T10:30:00Z" } ``` ## Rate limiting The API applies rate limiting to protect backend resources. The default limits are: | Scope | Limit | | ---------------------------------------- | --------------------------------------------------------- | | General endpoints | 100 requests per 60 seconds | | AI endpoints (`/api/ai/*`, `/api/rag/*`) | Stricter limits configured separately via `ai_rate_limit` | When you exceed the limit, the API returns `429 Too Many Requests`. You can configure the thresholds in your backend settings. ## Error responses The API uses standard HTTP status codes. All error bodies follow the same shape: ```json theme={null} { "detail": "error message" } ``` Common status codes you will encounter: | Status | Meaning | | ------ | -------------------------------------------------------- | | `400` | Bad request — invalid input or missing required field | | `401` | Unauthorized — missing or invalid token | | `403` | Forbidden — account inactive or insufficient permissions | | `404` | Not found — resource does not exist | | `422` | Unprocessable entity — request body failed validation | | `429` | Too many requests — rate limit exceeded | | `500` | Internal server error — unexpected backend error | ### Example error response ```json theme={null} { "detail": "Incorrect email or password" } ``` For validation errors (422), FastAPI returns a more detailed structure that includes the specific field and error type: ```json theme={null} { "detail": [ { "loc": ["body", "email"], "msg": "value is not a valid email address", "type": "value_error.email" } ] } ``` # RAG API: ingest documents and query with context Source: https://docs.shipfastai.dev/api-reference/rag Reference for POST /api/rag/ingest/text, POST /api/rag/ingest/file, POST /api/rag/search, and POST /api/rag/query for retrieval-augmented generation. The RAG (Retrieval-Augmented Generation) API lets you build knowledge-grounded AI features. You ingest text or files into a per-user vector store, then search or query that store to get LLM answers backed by your own documents. All endpoints are available on the **Pro** and **Enterprise** tiers and are mounted under `/api/rag/`. RAG endpoints are available on Pro and Enterprise plans only. The vector store is scoped per user — each user can only search and retrieve their own ingested documents. *** ## POST /api/rag/ingest/text Ingest raw text content into the vector store. The text is automatically split into overlapping chunks, embedded, and stored. Returns the generated document IDs and the number of chunks created. **Headers:** `Bearer ` **Request body:** The raw text to ingest. There is no enforced length limit, but very large documents will produce many chunks. Arbitrary key-value pairs attached to every chunk from this document. Useful for filtering later (e.g., `{"source": "faq", "topic": "billing"}`). Target character length of each chunk. Must be between `100` and `10000`. The chunker attempts to break at sentence boundaries near this length. Number of characters of overlap between adjacent chunks. Must be between `0` and `2000`. Overlap improves recall by ensuring context is not lost at chunk boundaries. ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/rag/ingest/text", headers={"Authorization": f"Bearer {access_token}"}, json={ "content": "Shipfastai is an AI-ready SaaS boilerplate for Python developers. It includes authentication, billing, and built-in RAG support out of the box.", "metadata": {"source": "product-docs", "topic": "overview"}, "chunk_size": 500, "chunk_overlap": 100, }, ) print(response.json()) ``` ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/rag/ingest/text \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "content": "Shipfastai is an AI-ready SaaS boilerplate for Python developers...", "metadata": {"source": "product-docs"}, "chunk_size": 500, "chunk_overlap": 100 }' ``` **Response** — `IngestResponse`: List of IDs assigned to each stored chunk. Each ID is a 12-character MD5 hash prefix plus the chunk index (e.g., `"a1b2c3d4e5f6_0"`). Total number of chunks the text was split into and stored. ```json theme={null} { "document_ids": [ "a1b2c3d4e5f6_0", "b2c3d4e5f6a1_1" ], "chunks_created": 2 } ``` *** ## POST /api/rag/ingest/file Ingest a file directly into the vector store. The file is parsed to plain text, then processed identically to `/api/rag/ingest/text`. Supported formats: `.txt`, `.pdf`, `.docx`. **Headers:** `Bearer ` **Form data (multipart/form-data):** The file to upload. Must be a `.txt`, `.pdf`, or `.docx` file. Target character length of each chunk. Must be between `100` and `10000`. Character overlap between adjacent chunks. Must be between `0` and `2000`. ```python Python theme={null} import requests with open("documentation.pdf", "rb") as f: response = requests.post( "http://localhost:8000/api/rag/ingest/file", headers={"Authorization": f"Bearer {access_token}"}, files={"file": ("documentation.pdf", f, "application/pdf")}, data={"chunk_size": 800, "chunk_overlap": 150}, ) print(response.json()) ``` ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/rag/ingest/file \ --header "Authorization: Bearer " \ --form file=@documentation.pdf \ --form chunk_size=800 \ --form chunk_overlap=150 ``` **Response** — `IngestResponse` (same shape as `/ingest/text`): ```json theme={null} { "document_ids": [ "c3d4e5f6a1b2_0", "d4e5f6a1b2c3_1", "e5f6a1b2c3d4_2" ], "chunks_created": 3 } ``` The `metadata` for file ingests automatically includes `{"source": ""}` in addition to any user-supplied metadata. *** ## POST /api/rag/search Perform a pure semantic search over the vector store without involving an LLM. Returns the most relevant chunks ranked by similarity score. Useful for building your own retrieval logic or debugging what is in the store. **Headers:** `Bearer ` **Request body:** The search query. The query is embedded and compared against stored chunk embeddings. Maximum number of results to return. Must be between `1` and `50`. Optional metadata filter to narrow results. Key-value pairs are matched against chunk metadata (e.g., `{"source": "product-docs"}`). The `user_id` filter is applied automatically — you do not need to include it. ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/rag/search", headers={"Authorization": f"Bearer {access_token}"}, json={ "query": "How does billing work?", "top_k": 3, "filter": {"topic": "billing"}, }, ) for result in response.json()["results"]: print(f"[{result['score']:.2f}] {result['content'][:120]}") ``` ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/rag/search \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "query": "How does billing work?", "top_k": 3, "filter": {"topic": "billing"} }' ``` **Response** — `SearchResponse`: Ordered list of matching chunks, most similar first. The chunk's document ID. The text content of the chunk. Cosine similarity score between `0.0` and `1.0`. Higher is more relevant. The metadata attached to this chunk at ingestion time, plus `user_id` and chunking info. ```json theme={null} { "results": [ { "id": "a1b2c3d4e5f6_0", "content": "Shipfastai integrates with Stripe for subscription billing...", "score": 0.91, "metadata": { "source": "product-docs", "topic": "billing", "chunk_index": 0, "total_chunks": 2, "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } }, { "id": "b2c3d4e5f6a1_1", "content": "You can manage your subscription through the Stripe customer portal...", "score": 0.84, "metadata": { "source": "product-docs", "topic": "billing", "chunk_index": 1, "total_chunks": 2, "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } ] } ``` *** ## POST /api/rag/query Ask a natural language question. The API retrieves the most relevant chunks from the vector store and passes them to the LLM as context, returning a grounded answer along with the source chunks used. Supports streaming and optional conversation history for multi-turn sessions. **Headers:** `Bearer ` **Request body:** The natural language question to answer using the ingested documents. Number of document chunks to retrieve as context. Must be between `1` and `50`. Minimum similarity score threshold between `0.0` and `1.0`. Chunks scoring below this value are excluded from the context passed to the LLM. When `true`, the answer is streamed as server-sent events (SSE), using the same format as the AI Chat streaming endpoint. Optional conversation history for multi-turn queries. Each item must have a `role` (`"user"` or `"assistant"`) and `content`. Providing history allows the model to resolve follow-up questions against prior context. Optional metadata filter applied during retrieval (e.g., `{"source": "faq"}`). The `user_id` filter is applied automatically. ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/rag/query", headers={"Authorization": f"Bearer {access_token}"}, json={ "question": "How do I cancel my subscription?", "top_k": 4, "min_score": 0.6, "filter": {"source": "product-docs"}, }, ) data = response.json() print(data["answer"]) print(f"\nSources used: {len(data['sources'])}") ``` ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/rag/query \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "question": "How do I cancel my subscription?", "top_k": 4, "min_score": 0.6, "filter": {"source": "product-docs"} }' ``` **Response** — `RAGQueryResponse`: The LLM-generated answer, grounded in the retrieved document chunks. The document chunks that were retrieved and used as context. Same shape as `SearchResponse.results`. Chunk document ID. Text content of the chunk. Similarity score used for retrieval. Metadata attached to the chunk. Token usage for the LLM call. May be `null` if the provider does not return usage data. ```json theme={null} { "answer": "You can cancel your subscription at any time through the Stripe customer portal. Navigate to your account settings and click 'Manage Subscription' to open the portal, where you can cancel, downgrade, or update your payment method.", "sources": [ { "id": "b2c3d4e5f6a1_1", "content": "You can manage your subscription through the Stripe customer portal...", "score": 0.88, "metadata": { "source": "product-docs", "chunk_index": 1, "total_chunks": 2, "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } ], "usage": { "prompt_tokens": 312, "completion_tokens": 58, "total_tokens": 370 } } ``` ### Multi-turn query example Use `chat_history` to maintain context across follow-up questions: ```python theme={null} import requests history = [] def ask(question: str) -> str: response = requests.post( "http://localhost:8000/api/rag/query", headers={"Authorization": f"Bearer {access_token}"}, json={"question": question, "chat_history": history}, ) data = response.json() history.append({"role": "user", "content": question}) history.append({"role": "assistant", "content": data["answer"]}) return data["answer"] print(ask("What is included in the Pro plan?")) print(ask("And how much does it cost?")) # resolved against prior context ``` *** ## DELETE /api/rag/documents/ Delete a specific document chunk from the vector store by its ID. The document chunk ID to delete, as returned by the ingest endpoints. ```bash theme={null} curl --request DELETE \ --url http://localhost:8000/api/rag/documents/a1b2c3d4e5f6_0 \ --header "Authorization: Bearer " ``` **Response:** ```json theme={null} { "status": "deleted", "document_id": "a1b2c3d4e5f6_0" } ``` # Users API: manage user profiles and accounts Source: https://docs.shipfastai.dev/api-reference/users Reference for all Users API endpoints: get and update your profile, change password or email, export your data, and delete your account. The Users API gives authenticated users full control over their own profile and account data. You can retrieve and update your profile, change credentials, request a GDPR-compliant data export, and permanently delete your account. All endpoints are mounted under `/api/users/` and require a valid Bearer token. All endpoints on this page require an `Authorization: Bearer ` header. See [Authentication](/api-reference/authentication) for how to obtain a token. *** ## GET /api/users/me Return the full profile of the currently authenticated user. ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/users/me \ --header "Authorization: Bearer " ``` **Response** — `UserResponse`: UUID of the user. The user's email address. The user's display name. URL to the user's profile picture. Whether the account is active. Whether the email address has been verified. The OAuth provider used to create the account (`google`, `github`), or `null` for password-based accounts. Current subscription status (e.g., `free`, `active`, `cancelled`). Current subscription tier (e.g., `free`, `pro`). ISO 8601 timestamp of account creation. ```json theme={null} { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "avatar_url": "https://example.com/avatars/jane.png", "is_active": true, "is_verified": true, "oauth_provider": null, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2024-01-15T10:30:00Z" } ``` *** ## PATCH /api/users/me Update the authenticated user's profile. Only the fields you include in the request body are updated; omitted fields are left unchanged. A new display name for the user. A new URL for the user's profile picture. ```bash curl theme={null} curl --request PATCH \ --url http://localhost:8000/api/users/me \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "full_name": "Jane A. Smith", "avatar_url": "https://example.com/avatars/jane-new.png" }' ``` ```python Python theme={null} import requests response = requests.patch( "http://localhost:8000/api/users/me", headers={"Authorization": f"Bearer {access_token}"}, json={ "full_name": "Jane A. Smith", "avatar_url": "https://example.com/avatars/jane-new.png", }, ) print(response.json()) ``` **Response** — `UserResponse` with updated fields: ```json theme={null} { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane A. Smith", "avatar_url": "https://example.com/avatars/jane-new.png", "is_active": true, "is_verified": true, "oauth_provider": null, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2024-01-15T10:30:00Z" } ``` *** ## POST /api/users/me/change-password Change the password for the currently authenticated user. This endpoint is only available to accounts that were created with a password. OAuth-only accounts (social login only, no password set) must use the forgot-password flow to set an initial password. The user's existing password. The new password to set. ```bash curl theme={null} curl --request POST \ --url http://localhost:8000/api/users/me/change-password \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "current_password": "old-password-123", "new_password": "new-password-456" }' ``` ```python Python theme={null} import requests response = requests.post( "http://localhost:8000/api/users/me/change-password", headers={"Authorization": f"Bearer {access_token}"}, json={ "current_password": "old-password-123", "new_password": "new-password-456", }, ) print(response.json()) ``` **Response:** ```json theme={null} { "message": "Password changed successfully" } ``` Returns `400` if the current password is incorrect, or if the account uses social login and has no password set. *** ## POST /api/users/me/change-email Request an email address change. The new address must not already be in use. After the change, `is_verified` is set to `false` and a new verification email is sent to the new address. The new email address to associate with the account. Must be a valid email format. The user's current password to confirm the change. Required for password-based accounts. ```bash theme={null} curl --request POST \ --url http://localhost:8000/api/users/me/change-email \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{ "new_email": "new-address@example.com", "password": "current-password-123" }' ``` **Response:** ```json theme={null} { "message": "Verification email sent to your new address" } ``` *** ## GET /api/users/me/export Export all personal data stored for the authenticated user as a downloadable JSON file. This endpoint is provided for GDPR compliance. ```bash theme={null} curl --request GET \ --url http://localhost:8000/api/users/me/export \ --header "Authorization: Bearer " \ --output user-data-export.json ``` The response is a `Content-Disposition: attachment` JSON file with the following fields: UUID of the user. Email address. Display name. Profile picture URL. OAuth provider if applicable. Email verification status. Current subscription status. Current subscription tier. Account creation timestamp. Last profile update timestamp. Most recent login timestamp. Timestamp of when this export was generated. ```json theme={null} { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "user@example.com", "full_name": "Jane Smith", "avatar_url": null, "oauth_provider": null, "is_verified": true, "subscription_status": "active", "subscription_tier": "pro", "created_at": "2024-01-15T10:30:00.000000", "updated_at": "2024-03-01T14:22:00.000000", "last_login_at": "2024-03-10T09:00:00.000000", "exported_at": "2024-03-10T09:05:00.000000" } ``` *** ## DELETE /api/users/me Permanently delete the authenticated user's account. This action is irreversible. For **password-based accounts**, you must supply the current password to confirm deletion. For **OAuth-only accounts** (no password), you must pass `"confirm": "DELETE"` instead. The user's current password. Required for password-based accounts. Must be the exact string `"DELETE"`. Required for OAuth-only accounts that have no password. Deleting an account is permanent. All user data is removed from the database immediately and cannot be recovered. ```bash Password account theme={null} curl --request DELETE \ --url http://localhost:8000/api/users/me \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{"password": "current-password-123"}' ``` ```bash OAuth account theme={null} curl --request DELETE \ --url http://localhost:8000/api/users/me \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --data '{"confirm": "DELETE"}' ``` **Response:** ```json theme={null} { "message": "User deleted successfully" } ``` # Database setup and connection configuration Source: https://docs.shipfastai.dev/configuration/database Configure PostgreSQL with SQLAlchemy, tune connection pooling, and run Alembic migrations for your Shipfastai app locally and in production. Shipfastai uses PostgreSQL as its primary database, accessed through SQLAlchemy with connection pooling enabled. You configure the connection via the `DATABASE_URL` environment variable. When developing locally, Docker Compose spins up a PostgreSQL instance for you automatically — no manual installation required. ## Connection string Set `DATABASE_URL` to a standard PostgreSQL connection string in your `.env` file: ``` postgresql://user:password@host:port/dbname ``` ```bash Local development theme={null} DATABASE_URL=postgresql://postgres:postgres@localhost:5432/Shipfastai ``` ```bash Railway theme={null} DATABASE_URL=postgresql://postgres:RaNdOmPaSsWoRd@containers-us-west-123.railway.app:6543/railway ``` ```bash Supabase (direct) theme={null} DATABASE_URL=postgresql://postgres:your-password@db.xxxxxxxxxxxx.supabase.co:5432/postgres ``` When using Docker Compose for local development, the backend container connects to the `db` service using `postgresql://postgres:postgres@db:5432/Shipfastai`. The `.env` file value is overridden by the `environment` block in `docker-compose.yml`. ## Connection pool settings SQLAlchemy maintains a pool of reusable database connections. Two variables control its size: | Variable | Default | Description | | ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `DATABASE_POOL_SIZE` | `5` | Number of connections kept open at all times. | | `DATABASE_MAX_OVERFLOW` | `10` | Additional connections allowed when the pool is exhausted. Maximum total connections = `POOL_SIZE + MAX_OVERFLOW`. | The defaults work well for most small-to-medium deployments. Increase `DATABASE_POOL_SIZE` when you observe connection wait times under sustained load, or when your hosting provider offers a larger connection limit. ```bash .env theme={null} DATABASE_POOL_SIZE=10 DATABASE_MAX_OVERFLOW=20 ``` Pool settings are ignored when `DATABASE_URL` starts with `sqlite://`. SQLite does not support connection pooling. ## Running migrations Shipfastai manages database schema changes through migration scripts. After setting up your environment, apply any pending migrations by running the following from the `backend/` directory of your tier: ```bash theme={null} cd products/basic/backend alembic upgrade head ``` Run this command whenever you pull an update that includes new migration files. ## Using Docker When you start the application with Docker Compose, PostgreSQL is provisioned automatically. You do not need a local PostgreSQL installation. ```bash theme={null} docker compose up ``` The `docker-compose.yml` file defines a `db` service running `postgres:15-alpine` on port `5432`. The backend service declares a `depends_on` health check, so it will not start until PostgreSQL is ready. Docker Compose handles the database for local development. You still need to run `alembic upgrade head` on first start or after pulling new migrations. The Docker entrypoint does not run migrations automatically. To connect to the database directly while Docker Compose is running: ```bash theme={null} docker exec -it -db-1 psql -U postgres -d Shipfastai ``` Replace `` with your Docker Compose project name (by default, the directory name). # Email configuration with Resend and Brevo Source: https://docs.shipfastai.dev/configuration/email Configure transactional email in Shipfastai with Resend. Send verification, welcome, and password reset emails, and test locally without a real API key. Shipfastai sends transactional emails through [Resend](https://resend.com) — a developer-focused email API. Emails are optional: if you leave `RESEND_API_KEY` unset, the application starts normally and logs a warning instead of sending. This makes local development easier because you can register and verify accounts without a real email provider. ## Resend setup To enable email sending, create a free Resend account at [resend.com](https://resend.com), then add your API key and sender address to `.env`: ```bash .env theme={null} RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx FROM_EMAIL=noreply@yourdomain.com ``` | Variable | Required | Description | | ---------------- | -------- | ------------------------------------------------------------------------------------- | | `RESEND_API_KEY` | No | Your Resend API key. Starts with `re_`. Leave empty to disable email. | | `FROM_EMAIL` | No | The sender address shown in all outgoing emails. Must be a verified domain in Resend. | Resend's free tier allows 3,000 emails per month and up to 100 emails per day. This is more than enough for development and early-stage production use. ## Email types sent Shipfastai sends three types of transactional emails automatically: | Trigger | Email sent | Description | | ---------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | User registers | Verification email | Contains a link to `{FRONTEND_URL}/verify-email?token=…`. The token expires after `VERIFICATION_TOKEN_EXPIRE_HOURS` (default: 24 hours). | | User verifies email | Welcome email | Sent immediately after successful verification. Includes a link to the dashboard. | | User requests password reset | Password reset email | Contains a link to `{FRONTEND_URL}/reset-password?token=…`. The token expires after `PASSWORD_RESET_TOKEN_EXPIRE_HOURS` (default: 1 hour). | The `FRONTEND_URL` variable controls the base URL used in all email links. Make sure it matches your deployed frontend address in production. ## Customizing email templates Email templates are defined in `backend/app/core/email.py` as inline HTML strings inside three methods on the `EmailService` class: | Method | Email | | --------------------------- | -------------------- | | `send_verification_email` | Verification email | | `send_welcome_email` | Welcome email | | `send_password_reset_email` | Password reset email | To customize a template, open `backend/app/core/email.py` and edit the `html` variable inside the corresponding method. The templates are plain HTML strings — you can replace them with any HTML you like, including full branded designs. For example, to change the verification email subject line: ```python backend/app/core/email.py theme={null} return self._send(to_email, "Confirm your account - MyApp", html) ``` If you want to use a templating library like Jinja2, install it and render the template before passing the resulting HTML string to `self._send`. ## Testing locally You have two options for testing email flows during local development: **Option 1: Leave `RESEND_API_KEY` empty (recommended)** When `RESEND_API_KEY` is not set, the `EmailService` logs the would-be email to the console instead of sending it. You can still test the full registration flow — check the backend logs to see the verification token URL. ``` WARNING Email not configured (missing RESEND_API_KEY). Would have sent to=user@example.com subject=Verify your email - Shipfastai ``` **Option 2: Use a Resend test API key** Resend supports sending emails to your own verified address using a real API key in test mode. Add your `re_` key to `.env` and set `FROM_EMAIL` to your verified sender. ```bash .env theme={null} RESEND_API_KEY=re_your_real_key FROM_EMAIL=you@yourdomain.com ``` # Environment variables reference for Shipfastai Source: https://docs.shipfastai.dev/configuration/environment-variables Reference for every Shipfastai environment variable: app settings, database, JWT auth, Stripe billing, email, OAuth providers, Redis, and rate limiting. Shipfastai reads all configuration from a `.env` file at the root of your product directory. Copy `.env.example` to `.env` and fill in your values before starting the application. Variables marked as required must be set or the backend will refuse to start. | Variable | Required | Example | Description | | -------------- | -------- | ----------------------- | -------------------------------------------------------------------- | | `APP_NAME` | No | `Shipfastai` | Display name used in logs and emails. Defaults to `Shipfastai`. | | `APP_ENV` | No | `development` | Runtime environment. Use `production` for deployed apps. | | `DEBUG` | No | `true` | Enables verbose error responses. Set to `false` in production. | | `FRONTEND_URL` | No | `http://localhost:3000` | Base URL of your Next.js frontend. Used to build email links. | | `BACKEND_URL` | No | `http://localhost:8000` | Base URL of your FastAPI backend. Used to build OAuth redirect URIs. | | `CORS_ORIGINS` | No | `http://localhost:3000` | Comma-separated list of allowed origins for CORS. | | `LOG_LEVEL` | No | `INFO` | Logging level: `DEBUG`, `INFO`, `WARNING`, `ERROR`. | | `LOG_FORMAT` | No | `text` | Log format: `text` or `json`. | | Variable | Required | Example | Description | | ----------------------- | -------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ | | `DATABASE_URL` | Yes | `postgresql://postgres:postgres@localhost:5432/Shipfastai` | Full PostgreSQL connection string. | | `DATABASE_POOL_SIZE` | No | `5` | Number of persistent connections kept open. Defaults to `5`. | | `DATABASE_MAX_OVERFLOW` | No | `10` | Extra connections allowed beyond `DATABASE_POOL_SIZE`. Defaults to `10`. | | Variable | Required | Example | Description | | ----------------------------------- | -------- | ----------------------- | --------------------------------------------------------------------------- | | `JWT_SECRET` | Yes | `your-super-secret-key` | Secret key used to sign JWT tokens. Use a long random string in production. | | `JWT_ALGORITHM` | No | `HS256` | Signing algorithm for JWT. Defaults to `HS256`. | | `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` | No | `30` | Lifetime of an access token in minutes. Defaults to `30`. | | `JWT_REFRESH_TOKEN_EXPIRE_DAYS` | No | `7` | Lifetime of a refresh token in days. Defaults to `7`. | | `VERIFICATION_TOKEN_EXPIRE_HOURS` | No | `24` | How long an email verification link stays valid. Defaults to `24`. | | `PASSWORD_RESET_TOKEN_EXPIRE_HOURS` | No | `1` | How long a password reset link stays valid. Defaults to `1`. | | Variable | Required | Example | Description | | ------------------------- | -------- | ------------- | --------------------------------------------------------------- | | `STRIPE_SECRET_KEY` | Yes | `sk_test_xxx` | Your Stripe secret key. Use `sk_test_` keys during development. | | `STRIPE_PUBLISHABLE_KEY` | Yes | `pk_test_xxx` | Your Stripe publishable key. Sent to the frontend. | | `STRIPE_WEBHOOK_SECRET` | Yes | `whsec_xxx` | Signing secret for verifying incoming Stripe webhook events. | | `STRIPE_PRICE_ID_MONTHLY` | No | `price_xxx` | Stripe price ID for your monthly subscription plan. | | `STRIPE_PRICE_ID_YEARLY` | No | `price_xxx` | Stripe price ID for your yearly subscription plan. | | Variable | Required | Example | Description | | ---------------- | -------- | ------------------------ | ----------------------------------------------------------------------------------- | | `RESEND_API_KEY` | No | `re_xxx` | API key from Resend. If omitted, email sending is disabled and a warning is logged. | | `FROM_EMAIL` | No | `noreply@yourdomain.com` | Sender address for all transactional emails. | | Variable | Required | Example | Description | | ---------------------- | -------- | --------------------------------------- | ---------------------------------------------------------------- | | `GOOGLE_CLIENT_ID` | No | `1234567890.apps.googleusercontent.com` | Google OAuth client ID. Leave empty to disable Google login. | | `GOOGLE_CLIENT_SECRET` | No | `GOCSPX-xxx` | Google OAuth client secret. | | `GITHUB_CLIENT_ID` | No | `Ov23liXXXXXX` | GitHub OAuth App client ID. Leave empty to disable GitHub login. | | `GITHUB_CLIENT_SECRET` | No | `abc123xxx` | GitHub OAuth App client secret. | | Variable | Required | Example | Description | | ----------- | -------- | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `REDIS_URL` | No | `redis://localhost:6379` | Connection URL for Redis. Used for caching and rate limiting. Docker Compose starts Redis automatically. | | Variable | Required | Example | Description | | --------------------- | -------- | ------- | ----------------------------------------------------------------- | | `RATE_LIMIT_REQUESTS` | No | `100` | Maximum number of requests allowed per window. Defaults to `100`. | | `RATE_LIMIT_WINDOW` | No | `60` | Duration of the rate limit window in seconds. Defaults to `60`. | These variables are prefixed with `NEXT_PUBLIC_` and are bundled into the browser at build time. | Variable | Required | Example | Description | | ------------------------------------- | -------- | ----------------------- | ------------------------------------------------------------------- | | `NEXT_PUBLIC_API_URL` | No | `http://localhost:8000` | Base URL of the backend API, used by the frontend to make requests. | | `NEXT_PUBLIC_STRIPE_PRICE_ID_MONTHLY` | No | `price_xxx` | Stripe monthly price ID exposed to the frontend for checkout. | | `NEXT_PUBLIC_STRIPE_PRICE_ID_YEARLY` | No | `price_xxx` | Stripe yearly price ID exposed to the frontend for checkout. | ## Complete .env.example The following is the full `.env.example` from the basic product tier. Copy it to `.env` and replace placeholder values with your own. ```bash .env theme={null} # ============================================================ # Shipfastai Environment Configuration # ============================================================ # Copy this file to .env and fill in your values # ============================================================ # Application APP_NAME=Shipfastai APP_ENV=development DEBUG=true FRONTEND_URL=http://localhost:3000 BACKEND_URL=http://localhost:8000 # Database DATABASE_URL=postgresql://postgres:postgres@localhost:5432/Shipfastai DATABASE_POOL_SIZE=5 DATABASE_MAX_OVERFLOW=10 # JWT Authentication JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_ALGORITHM=HS256 JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 # Token expiry VERIFICATION_TOKEN_EXPIRE_HOURS=24 PASSWORD_RESET_TOKEN_EXPIRE_HOURS=1 # OAuth - Google GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= # OAuth - GitHub GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= # Stripe STRIPE_SECRET_KEY=sk_test_xxx STRIPE_PUBLISHABLE_KEY=pk_test_xxx STRIPE_WEBHOOK_SECRET=whsec_xxx STRIPE_PRICE_ID_MONTHLY=price_xxx STRIPE_PRICE_ID_YEARLY=price_xxx # Email (Resend) RESEND_API_KEY= FROM_EMAIL=noreply@yourdomain.com # Redis (optional, for caching) REDIS_URL=redis://localhost:6379 # Sentry (optional, for error tracking) SENTRY_DSN= # Rate Limiting RATE_LIMIT_REQUESTS=100 RATE_LIMIT_WINDOW=60 # CORS CORS_ORIGINS=http://localhost:3000 # Logging LOG_LEVEL=INFO LOG_FORMAT=text # Frontend env vars (Next.js) NEXT_PUBLIC_API_URL=http://localhost:8000 NEXT_PUBLIC_STRIPE_PRICE_ID_MONTHLY=price_xxx NEXT_PUBLIC_STRIPE_PRICE_ID_YEARLY=price_xxx ``` # Get started with Shipfastai in minutes Source: https://docs.shipfastai.dev/configuration/oauth Clone the repository, configure your environment variables, and run the full Shipfastai stack locally using Docker Compose in under 10 minutes. This guide walks you through getting Shipfastai running on your machine for the first time. By the end, you will have the full stack running locally — FastAPI backend on port 8000, Next.js frontend on port 3000, PostgreSQL via Supabase, and Redis — all orchestrated by Docker Compose. After purchase, you receive instant access to a private GitHub repository. Clone it to your local machine: ```bash theme={null} git clone https://github.com/Shipfastai/boilerplate.git cd shipfastai ``` If you purchased the Basic tier, work from the `products/basic/` directory. Pro and Enterprise tiers each have their own directory (`products/pro/` and `products/enterprise/`). ```bash theme={null} cd products/basic ``` Copy the example environment file and fill in your credentials: ```bash theme={null} cp .env.example .env ``` Open `.env` and set the following required variables before starting the app: | Variable | Description | | ------------------------------- | ------------------------------------------------------------ | | `NEXT_PUBLIC_SUPABASE_URL` | Your Supabase project URL | | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Your Supabase anonymous/public key | | `SUPABASE_SERVICE_ROLE_KEY` | Your Supabase service role key (keep secret!) | | `STRIPE_SECRET_KEY` | Your Stripe secret key (`sk_test_...` for development) | | `STRIPE_WEBHOOK_SECRET` | Your Stripe webhook signing secret (`whsec_...`) | | `STRIPE_BASIC_PRICE_ID` | Stripe price ID for Basic tier | | `STRIPE_PRO_PRICE_ID` | Stripe price ID for Pro tier | | `STRIPE_ENTERPRISE_PRICE_ID` | Stripe price ID for Enterprise tier | | `BREVO_API_KEY` | API key from [Brevo](https://brevo.com) for marketing emails | | `NEXT_PUBLIC_APP_URL` | Your app URL (e.g., `http://localhost:3000`) | The Supabase variables in `.env.example` connect to your Supabase project. Create a free project at [supabase.com](https://supabase.com) if you haven't already. ```bash theme={null} # Supabase configuration NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-service-role-key ``` Start the full stack in the background with a single command: ```bash theme={null} docker-compose up -d ``` Docker Compose starts the services: the FastAPI backend, the Next.js frontend, and Redis. On first run it will build the images, which takes a few minutes. Subsequent starts are fast. To watch the logs: ```bash theme={null} docker-compose logs -f ``` To stop all services: ```bash theme={null} docker-compose down ``` Once all containers are running, open the following URLs in your browser: | Service | URL | | ---------------------------- | ---------------------------------------------------------- | | Frontend | [http://localhost:3000](http://localhost:3000) | | Backend API | [http://localhost:8000](http://localhost:8000) | | Interactive API docs | [http://localhost:8000/docs](http://localhost:8000/docs) | | Alternative API docs (ReDoc) | [http://localhost:8000/redoc](http://localhost:8000/redoc) | The interactive API docs at `/docs` are generated automatically by FastAPI from your route definitions. Use them to test endpoints directly from the browser without any additional tooling. To run the stack without Docker, start the backend and frontend in separate terminals: ```bash theme={null} # Terminal 1 — backend cd backend python -m venv venv source venv/bin/activate # on Windows: venv\Scripts\activate pip install -r requirements.txt uvicorn app.main:app --reload --port 8000 ``` ```bash theme={null} # Terminal 2 — frontend cd frontend pnpm install pnpm dev ``` You'll need a running Supabase project for the database. The free tier at [supabase.com](https://supabase.com) is sufficient for local development. The variables above cover the minimum required to start the app. For a full reference of every environment variable — including optional settings for Redis, email configuration, and deployment settings — see the [Configuration](/configuration/environment-variables) section. # Deploy Shipfastai to AWS with Terraform Source: https://docs.shipfastai.dev/deployment/aws-terraform Provision a production AWS environment for Shipfastai using included Terraform modules for ECS Fargate, RDS PostgreSQL, and an Application Load Balancer. Shipfastai's Enterprise tier includes a complete set of Terraform modules that provision a production-ready AWS environment. The infrastructure runs your backend and frontend as containerized services on ECS Fargate, backed by a managed RDS PostgreSQL database, and fronted by an Application Load Balancer with TLS termination. This page explains what is provisioned, how to deploy it, and how to run database migrations once your services are running. The Terraform infrastructure described on this page is included exclusively in the **Enterprise tier**. If you are on the Basic or Pro tier, refer to the [Railway & Vercel](/deployment/railway-vercel) guide instead. Estimated AWS costs for a small production workload (2 Fargate tasks each for backend and frontend, a `db.t3.medium` RDS instance, one NAT Gateway, and an ALB) are roughly **\$50–100 per month**. Costs scale with traffic, task count, and data transfer. Use the [AWS Pricing Calculator](https://calculator.aws) to estimate your specific configuration before deploying. ## Prerequisites Before running Terraform, make sure you have the following installed and configured on your machine: * **AWS CLI** configured with credentials that have sufficient IAM permissions to create VPCs, ECS clusters, RDS instances, ECR repositories, and IAM roles. Run `aws sts get-caller-identity` to verify your credentials. * **Terraform 1.5 or later**. Run `terraform version` to check. * **Docker**, for building and pushing your backend and frontend images to ECR. ## What the Terraform modules provision The modules in `products/enterprise/infra/terraform/` create the following AWS resources: | Resource | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **VPC** | Isolated network with public and private subnets across two Availability Zones, a NAT Gateway, and an Internet Gateway | | **ECS cluster** | Fargate cluster with Container Insights enabled; runs the backend API service and the frontend web service | | **ECR repositories** | Two private image registries — one for the backend, one for the frontend — with image scanning on push | | **RDS PostgreSQL** | PostgreSQL 15 instance (`db.t3.medium` by default) in private subnets, with Multi-AZ enabled in production, automated backups, and deletion protection | | **Application Load Balancer** | Internet-facing ALB in public subnets; routes `/api` traffic to the backend target group and all other traffic to the frontend target group | | **Auto Scaling** | CPU-based auto scaling for both the API service (2–10 tasks) and the web service (2–6 tasks) | | **CloudWatch** | Log groups for both ECS services, plus alarms for high CPU and error rates | | **Secrets Manager** | A single secret that holds sensitive runtime variables injected into ECS task definitions | ## Initialize and deploy ```bash theme={null} cd products/enterprise/infra/terraform ``` Copy the example variables file and fill in your values: ```bash theme={null} cp terraform.tfvars.example terraform.tfvars ``` Open `terraform.tfvars` and set at minimum: ```hcl theme={null} aws_region = "us-east-1" environment = "production" app_name = "shipfastai-enterprise" db_password = "a-strong-random-password" domain_name = "api.yourdomain.com" ``` You can also override instance sizes, task counts, and auto-scaling limits. Refer to `variables.tf` for the full list of inputs and their defaults. ```bash theme={null} terraform init ``` This downloads the AWS provider and configures the S3 backend for remote state. Before running this command, create an S3 bucket for Terraform state and update the `backend "s3"` block in `main.tf` with your bucket name and key. ```bash theme={null} terraform plan ``` Review the output carefully. Terraform will list every resource it intends to create. Verify that the region, instance sizes, and CIDR blocks match your expectations before proceeding. ```bash theme={null} terraform apply ``` Type `yes` when prompted. The apply takes approximately 10–15 minutes, with the RDS instance taking the longest to become available. When it completes, Terraform prints the ALB DNS name, ECR repository URLs, and RDS endpoint as outputs. ## Pushing Docker images After `terraform apply` succeeds, build your images locally and push them to the ECR repositories Terraform created. The repository URLs are available in the Terraform outputs. ```bash theme={null} # Authenticate Docker with ECR aws ecr get-login-password --region us-east-1 | \ docker login --username AWS --password-stdin \ .dkr.ecr.us-east-1.amazonaws.com # Build and push the backend image docker build -t shipfastai-backend ./products/enterprise/backend docker tag shipfastai-backend:latest \ :latest docker push :latest # Build and push the frontend image docker build -t shipfastai-frontend ./products/enterprise/frontend docker tag shipfastai-frontend:latest \ :latest docker push :latest ``` Replace `` and `` with the values from the `backend_ecr_url` and `frontend_ecr_url` Terraform outputs. After pushing, force a new deployment so ECS pulls the updated images: ```bash theme={null} aws ecs update-service \ --cluster shipfastai-enterprise-cluster \ --service shipfastai-enterprise-api \ --force-new-deployment aws ecs update-service \ --cluster shipfastai-enterprise-cluster \ --service shipfastai-enterprise-web \ --force-new-deployment ``` ## Running migrations on AWS You need to run Alembic migrations whenever you deploy a new schema version. There are two common approaches. **Run migrations as a one-off ECS task.** Use the AWS CLI to start a task that overrides the container command with the Alembic upgrade command: ```bash theme={null} aws ecs run-task \ --cluster shipfastai-enterprise-cluster \ --task-definition shipfastai-enterprise-api \ --launch-type FARGATE \ --network-configuration "awsvpcConfiguration={subnets=[],securityGroups=[],assignPublicIp=DISABLED}" \ --overrides '{"containerOverrides":[{"name":"api","command":["alembic","upgrade","head"]}]}' ``` Retrieve the private subnet IDs and security group ID from the Terraform outputs (`private_subnet_ids` and the ECS tasks security group). **Use a bastion host for interactive access.** If you prefer to connect directly to the database, launch a small EC2 instance in a public subnet within the same VPC, configure its security group to allow inbound SSH, and add a rule to the RDS security group allowing connections from the bastion instance on port 5432. You can then SSH in and run Alembic from there. For automated deployments, the ECS task approach is recommended because it requires no additional infrastructure and uses the same Docker image as your running service. # Docker and Docker Compose local development setup Source: https://docs.shipfastai.dev/deployment/docker Run the full Shipfastai stack locally with Docker Compose, including the FastAPI backend, Next.js frontend, PostgreSQL, and Redis. Shipfastai ships with Docker Compose configuration so you can spin up the entire stack — FastAPI backend, Next.js frontend, PostgreSQL database, and Redis — with a single command. This page walks you through starting, inspecting, and stopping your local environment, and explains how the production Compose file differs. ## Starting the stack From the root of your product directory (e.g. `products/basic/` or `products/pro/`), run: ```bash theme={null} docker-compose up -d ``` Docker Compose pulls any missing images, builds the backend and frontend containers from their respective `Dockerfile`s, and starts all services in the background. Once the command returns, the following services are running: | Service | Port | Description | | ---------- | ---- | ----------------------------------- | | `frontend` | 3000 | Next.js development server | | `backend` | 8000 | FastAPI with Uvicorn, hot-reload on | | `db` | 5432 | PostgreSQL 15 | | `redis` | 6379 | Redis 7 for caching | The backend waits for PostgreSQL and Redis to pass their health checks before it starts, so all services are ready by the time `docker-compose up -d` returns. Run `docker-compose ps` at any time to see the status of every service and confirm each one is healthy before you start developing. ## Viewing logs To follow the combined log output of all services: ```bash theme={null} docker-compose logs -f ``` To tail only the backend: ```bash theme={null} docker-compose logs -f backend ``` You can substitute any service name (`frontend`, `db`, `redis`) to isolate its output. Press `Ctrl+C` to stop following without stopping the containers. ## Stopping the stack To stop all containers while preserving your database volume: ```bash theme={null} docker-compose down ``` To stop all containers **and delete all data volumes** (a full reset): ```bash theme={null} docker-compose down -v ``` The `-v` flag removes the named `postgres_data` volume, so your database is wiped. Use this when you want a clean slate or need to re-run migrations from scratch. ## Production Docker Compose The repository also includes `docker-compose.prod.yml` for production deployments on a single server (e.g. a DigitalOcean Droplet or EC2 instance). It differs from the development file in several important ways: * **No hot-reload.** The backend runs Gunicorn with multiple Uvicorn workers instead of `uvicorn --reload`, and the frontend is served as a pre-built static bundle. * **Pre-built images.** Services reference tagged Docker images (e.g. `ghcr.io/Shipfastai/backend:latest`) rather than building from local source. * **Production environment variables.** `APP_ENV=production`, `DEBUG=false`, and `LOG_FORMAT=json` are set explicitly. * **Nginx reverse proxy.** An Nginx container handles TLS termination and routes traffic to the backend (port 8000) and frontend (port 3000) services. * **Persistent Redis volume.** Redis data is written to a named volume so it survives container restarts. To start the production stack: ```bash theme={null} docker-compose -f docker-compose.prod.yml up -d ``` ## Rebuilding after changes When you modify the `Dockerfile` or install new Python/Node dependencies, rebuild the affected image before restarting: ```bash theme={null} docker-compose up -d --build ``` To rebuild a single service without restarting the others: ```bash theme={null} docker-compose up -d --build backend ``` # Deploy Shipfastai to Railway and Vercel Source: https://docs.shipfastai.dev/deployment/railway-vercel Deploy your FastAPI backend to Railway and your Next.js frontend to Vercel for a fully managed, zero-ops production environment. Railway and Vercel together give you a production-grade deployment with managed infrastructure, automatic TLS, and preview environments — without configuring servers. Railway hosts your FastAPI backend and PostgreSQL database, and Vercel hosts your Next.js frontend. This guide walks you through both deployments and explains how to wire them together. Railway's free Hobby plan is suitable for experimenting, but it imposes usage limits and may sleep inactive services. For a real production workload, upgrade to Railway's Pro plan (\$20/month) to get always-on services, more compute, and priority support. ## Deploy the backend to Railway Log in to [railway.app](https://railway.app) and create a new project. Inside the project, click **New** → **Database** → **Add PostgreSQL**. Railway provisions a managed PostgreSQL instance and automatically injects a `DATABASE_URL` variable into services in the same project. Click **New** → **GitHub Repo**, authorize Railway to access your repository, and select it. In the service settings, set the **Root Directory** to the backend path for your tier: * Basic: `products/basic/backend` * Pro: `products/pro/backend` * Enterprise: `products/enterprise/backend` Railway detects the `Dockerfile` in that directory and uses it for builds. In the service's **Variables** tab, add the following. `DATABASE_URL` is already injected by the linked PostgreSQL service — do not override it. | Variable | Description | | ------------------------- | --------------------------------------------------------- | | `JWT_SECRET` | A long, random string used to sign JWT tokens | | `STRIPE_SECRET_KEY` | Your Stripe secret key (`sk_live_...`) | | `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret (`whsec_...`) | | `STRIPE_PRICE_ID_MONTHLY` | Stripe price ID for the monthly plan | | `STRIPE_PRICE_ID_YEARLY` | Stripe price ID for the yearly plan | | `RESEND_API_KEY` | Resend API key for transactional email | | `FROM_EMAIL` | Sender address for outgoing email | | `FRONTEND_URL` | Your Vercel frontend URL (set after frontend is deployed) | | `CORS_ORIGINS` | Same as `FRONTEND_URL` | | `APP_ENV` | Set to `production` | | `DEBUG` | Set to `false` | In the service settings under **Deploy**, set the **Start Command** to: ```bash theme={null} uvicorn app.main:app --host 0.0.0.0 --port $PORT ``` Railway injects the `$PORT` variable automatically. Do not hardcode a port number. Click **Deploy**. Railway builds the Docker image and starts the service. Once the deployment is healthy, open the **Settings** tab and copy the public domain (e.g. `https://your-project-production.up.railway.app`). You will need this URL when setting up the frontend. ## Deploy the frontend to Vercel Log in to [vercel.com](https://vercel.com), click **Add New Project**, and import your GitHub repository. Vercel detects the monorepo structure automatically. In the project configuration, expand **Root Directory** and set it to the frontend path for your tier: * Basic: `products/basic/frontend` * Pro: `products/pro/frontend` * Enterprise: `products/enterprise/frontend` Vercel uses this directory as the build context and automatically detects Next.js. Before deploying, add the following environment variables in the Vercel dashboard under **Settings** → **Environment Variables**: | Variable | Value | | ------------------------------------- | -------------------------------------------------- | | `NEXT_PUBLIC_API_URL` | Your Railway backend URL from the previous section | | `NEXT_PUBLIC_STRIPE_PRICE_ID_MONTHLY` | Stripe monthly price ID | | `NEXT_PUBLIC_STRIPE_PRICE_ID_YEARLY` | Stripe yearly price ID | Variables prefixed with `NEXT_PUBLIC_` are embedded into the browser bundle at build time, so you must redeploy after changing them. Click **Deploy**. Vercel builds the Next.js application and publishes it to its global edge network. Once deployment succeeds, Vercel assigns a `.vercel.app` domain you can use immediately or replace with a custom domain. ## Custom domain setup You can add a custom domain in both services from their respective dashboards. * **Railway**: Go to your service → **Settings** → **Networking** → **Custom Domain**. Add your domain and update your DNS records as instructed. * **Vercel**: Go to your project → **Settings** → **Domains**. Add your domain and follow the DNS verification steps. After pointing your custom domain to the backend, update the `FRONTEND_URL` and `CORS_ORIGINS` variables on Railway, and redeploy the backend. After pointing your custom domain to the frontend, update `NEXT_PUBLIC_API_URL` on Vercel and redeploy the frontend. ## Environment variable checklist Use this table to confirm every variable is configured in the right service before going live. | Variable | Set on Railway | Set on Vercel | | ------------------------------------- | :------------: | :-----------: | | `DATABASE_URL` | Auto-injected | — | | `JWT_SECRET` | Yes | — | | `STRIPE_SECRET_KEY` | Yes | — | | `STRIPE_WEBHOOK_SECRET` | Yes | — | | `STRIPE_PRICE_ID_MONTHLY` | Yes | — | | `STRIPE_PRICE_ID_YEARLY` | Yes | — | | `RESEND_API_KEY` | Yes | — | | `FROM_EMAIL` | Yes | — | | `FRONTEND_URL` | Yes | — | | `CORS_ORIGINS` | Yes | — | | `APP_ENV` | Yes | — | | `NEXT_PUBLIC_API_URL` | — | Yes | | `NEXT_PUBLIC_STRIPE_PRICE_ID_MONTHLY` | — | Yes | | `NEXT_PUBLIC_STRIPE_PRICE_ID_YEARLY` | — | Yes | # LLM chat and AI completions with multiple providers Source: https://docs.shipfastai.dev/features/ai-llm Send chat messages and text completions to OpenAI, Anthropic, or Gemini with streaming support, rate limiting, and a unified API surface. The Pro and Enterprise tiers ship with a unified LLM layer that lets you talk to multiple AI providers through a single set of endpoints. You can send multi-turn chat messages, stream token-by-token responses via Server-Sent Events, or generate one-shot text completions — all with the same request shape. Switching providers is a single field change in your request body. All AI endpoints live under `/api/ai` and are protected by authentication and rate limiting. The AI and LLM features require the **Pro** or **Enterprise** tier. Requests from Basic tier accounts will be rejected with `403 Forbidden`. ## Sending a chat message Send a `POST` request to `/api/ai/chat` with a `messages` array following the OpenAI-style role format. Choose your provider, model, and generation parameters. Set `stream: false` (the default) to receive the full response at once. ```json Request theme={null} POST /api/ai/chat Authorization: Bearer Content-Type: application/json { "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "What is the capital of France?" } ], "provider": "openai", "model": "gpt-4o", "temperature": 0.7, "max_tokens": 500, "stream": false } ``` ```json Response — 200 OK theme={null} { "content": "The capital of France is Paris.", "model": "gpt-4o", "usage": { "prompt_tokens": 28, "completion_tokens": 9, "total_tokens": 37 } } ``` The full `ChatRequest` schema: | Field | Type | Default | Description | | ------------- | -------- | ---------------- | --------------------------------------------------------- | | `messages` | `array` | required | List of `{ role, content }` message objects. | | `provider` | `string` | `"openai"` | AI provider: `openai`, `anthropic`, or `gemini`. | | `model` | `string` | provider default | Model name (e.g. `gpt-4o`, `claude-3-5-sonnet-20241022`). | | `temperature` | `float` | `0.7` | Sampling temperature between `0.0` and `2.0`. | | `max_tokens` | `int` | `1000` | Maximum tokens to generate (1–16384). | | `stream` | `bool` | `false` | Set to `true` to receive a streaming response. | ## Streaming responses Set `stream: true` in your request to receive a `text/event-stream` response. Each event carries a single token. The stream ends with a `[DONE]` sentinel. ```json Streaming request theme={null} { "messages": [{ "role": "user", "content": "Tell me a joke." }], "provider": "openai", "model": "gpt-4o", "stream": true } ``` Each chunk arrives as a Server-Sent Event: ``` data: {"token": "Why"} data: {"token": " don"} data: {"token": "'t"} data: {"token": " scientists"} data: [DONE] ``` Consume the stream in JavaScript using `EventSource` or the `fetch` API with a `ReadableStream`: ```javascript Consuming SSE in JavaScript theme={null} const response = await fetch("/api/ai/chat", { method: "POST", headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ messages, provider: "openai", stream: true }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); for (const line of text.split("\n")) { if (!line.startsWith("data: ")) continue; const payload = line.slice(6); if (payload === "[DONE]") break; const { token } = JSON.parse(payload); process.stdout.write(token); } } ``` ## Text completions For single-turn generation from a plain-text prompt, use `POST /api/ai/completions`. You can optionally provide a `system_prompt` to set context. ```json Request theme={null} POST /api/ai/completions Authorization: Bearer Content-Type: application/json { "prompt": "Write a one-sentence summary of the Python programming language.", "system_prompt": "You write concise technical summaries.", "provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "temperature": 0.3, "max_tokens": 100 } ``` ```json Response — 200 OK theme={null} { "completion": "Python is a high-level, dynamically typed programming language known for its readable syntax and broad ecosystem of libraries.", "model": "claude-3-5-sonnet-20241022", "usage": { "input_tokens": 25, "output_tokens": 24 } } ``` ## Supported providers Set the `provider` field in any request to switch between backends. The model name must be valid for the chosen provider. ```json theme={null} { "provider": "openai", "model": "gpt-4o", "messages": [{ "role": "user", "content": "Hello!" }] } ``` Requires `OPENAI_API_KEY` in your environment. Supported models include `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`, and `gpt-3.5-turbo`. ```json theme={null} { "provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "messages": [{ "role": "user", "content": "Hello!" }] } ``` Requires `ANTHROPIC_API_KEY` in your environment. Supported models include `claude-3-5-sonnet-20241022`, `claude-3-opus-20240229`, and `claude-3-haiku-20240307`. ```json theme={null} { "provider": "gemini", "model": "gemini-1.5-pro", "messages": [{ "role": "user", "content": "Hello!" }] } ``` Requires `GOOGLE_API_KEY` in your environment. Supported models include `gemini-1.5-pro` and `gemini-1.5-flash`. # Authentication with JWT and OAuth providers Source: https://docs.shipfastai.dev/features/authentication Secure your app with JWT access and refresh tokens, OAuth login via Google and GitHub, email verification, and password reset flows. Shipfastai includes a complete authentication system out of the box. You get JWT-based login with short-lived access tokens and long-lived refresh tokens, OAuth sign-in with Google and GitHub, email verification on registration, and a two-step password reset flow. All auth endpoints live under the `/api/auth` prefix. Access tokens expire after **30 minutes** by default. Your client must use the refresh token endpoint to obtain a new access token before the current one expires, or prompt the user to log in again. ## Registering a user Send a `POST` request to `/api/auth/register` with the user's email, password, and full name. On success, the server creates the account, sends a verification email, and returns the new user object. The user is not yet verified at this point. ```json Request theme={null} POST /api/auth/register Content-Type: application/json { "email": "ada@example.com", "password": "hunter2secret", "full_name": "Ada Lovelace" } ``` ```json Response — 200 OK theme={null} { "id": "a1b2c3d4-0000-0000-0000-000000000001", "email": "ada@example.com", "full_name": "Ada Lovelace", "is_verified": false, "is_active": true, "created_at": "2026-04-09T10:00:00Z" } ``` ## Logging in Send a `POST` request to `/api/auth/login` with the user's credentials. A successful response contains an `access_token`, a `refresh_token`, and the user object. ```json Request theme={null} POST /api/auth/login Content-Type: application/json { "email": "ada@example.com", "password": "hunter2secret" } ``` ```json Response — 200 OK theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "a1b2c3d4-0000-0000-0000-000000000001", "email": "ada@example.com", "full_name": "Ada Lovelace", "is_verified": false, "is_active": true } } ``` Pass the `access_token` in the `Authorization: Bearer ` header on every subsequent request that requires authentication. ## Refreshing tokens Access tokens are short-lived. When one expires, send the stored `refresh_token` to `/api/auth/refresh` to get a new pair of tokens without requiring the user to log in again. ```json Request theme={null} POST /api/auth/refresh Content-Type: application/json { "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` ```json Response — 200 OK theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` Refresh tokens are valid for **7 days**. After expiry, the user must log in again. ## OAuth login To start an OAuth flow, direct the user's browser to `GET /api/auth/oauth/{provider}` where `{provider}` is either `google` or `github`. The endpoint returns an authorization URL and a state token. ```json Response — 200 OK theme={null} { "authorization_url": "https://accounts.google.com/o/oauth2/auth?client_id=...", "state": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` Redirect the user to `authorization_url`. After the user grants access, the provider sends them back to `GET /api/auth/callback/{provider}?code=...&state=...`. The backend exchanges the code for the user's profile, creates or links the account, then issues tokens and redirects to: ``` {FRONTEND_URL}/auth/callback?access_token=&refresh_token= ``` OAuth users are automatically marked as verified. Accounts are matched by provider ID first, then by email address, so an existing email/password account is linked automatically if the OAuth email matches. ## Email verification After registering, the user receives an email containing a verification link. That link includes a short-lived token (valid for **24 hours**). Submit that token to verify the account. ```json Request theme={null} POST /api/auth/verify-email Content-Type: application/json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` ```json Response — 200 OK theme={null} { "message": "Email verified successfully" } ``` If the verification email expired or was lost, you can trigger a resend: ```json Request theme={null} POST /api/auth/resend-verification Content-Type: application/json { "email": "ada@example.com" } ``` The endpoint always returns `200` regardless of whether the email is registered, to prevent user enumeration. ## Password reset Password reset is a two-step process. **Step 1 — request a reset link** ```json Request theme={null} POST /api/auth/forgot-password Content-Type: application/json { "email": "ada@example.com" } ``` ```json Response — 200 OK theme={null} { "message": "If your email is registered, a password reset link has been sent" } ``` The user receives an email with a reset link containing a token that is valid for **1 hour**. **Step 2 — submit the new password** ```json Request theme={null} POST /api/auth/reset-password Content-Type: application/json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "new_password": "newstrongerpassword" } ``` ```json Response — 200 OK theme={null} { "message": "Password reset successfully" } ``` You can optionally validate the token before showing the form by calling `POST /api/auth/verify-reset-token` with `{ "token": "..." }`. It returns `{ "valid": true }` if the token is still usable. # Multi-tenancy and tenant isolation Source: https://docs.shipfastai.dev/features/multi-tenancy Run isolated workspaces for multiple customers on a single deployment using PostgreSQL schema separation, RBAC, and per-tenant configuration. The Enterprise tier adds a full multi-tenancy layer to Shipfastai. Each tenant gets a dedicated PostgreSQL schema containing its own `documents`, `chat_sessions`, `chat_messages`, and `fine_tune_jobs` tables. Tenant metadata — names, plans, member lists, and API keys — lives in the shared `public` schema. A FastAPI middleware layer resolves the current tenant on every request before any handler code runs, and an RBAC system controls what each member of a tenant can do. Multi-tenancy is an **Enterprise tier** feature. It is not available in the Basic or Pro tiers. Attempting to enable tenant middleware without an Enterprise license will not provide the schema provisioning, RBAC, or audit logging features described on this page. ## How tenant isolation works When a tenant is created, Shipfastai automatically provisions a new PostgreSQL schema named `{prefix}{slug}` (for example, `tenant_acme`). All per-tenant application data is written into that schema, not the `public` schema. This means two tenants can never read each other's documents or chat sessions, even through direct database queries. The `TenantMiddleware` identifies the current tenant on every inbound request using one of three strategies, checked in priority order: 1. **Header** — `X-Tenant-ID` (UUID) or `X-Tenant-Slug` (slug string). 2. **Subdomain** — `acme.api.example.com` resolves to slug `acme`. 3. **Path prefix** — `/api/v1/acme/...` resolves to slug `acme`. After resolving a candidate tenant, the middleware validates it against the database and stores the result in `request.state.tenant`. If the tenant is suspended, the request is rejected with `403 Forbidden`. Public paths such as `/health`, `/docs`, and `/api/v1/auth` are excluded from tenant resolution. ## Tenant API endpoints All tenant management endpoints live under `/api/tenants`. Every endpoint requires an authenticated user; the RBAC layer then enforces what that user is allowed to do within the tenant. | Method | Path | Description | | -------- | ----------------------------------- | ---------------------------------------------------------------- | | `POST` | `/api/tenants/` | Create a new tenant. The caller becomes the owner. | | `GET` | `/api/tenants/` | List tenants the current user is a member of. | | `GET` | `/api/tenants/{tenant_id}` | Get details for a single tenant. | | `PATCH` | `/api/tenants/{tenant_id}` | Update tenant name, plan, or settings. Requires `tenants:write`. | | `DELETE` | `/api/tenants/{tenant_id}` | Suspend a tenant (soft delete). Requires `tenants:delete`. | | `POST` | `/api/tenants/{tenant_id}/transfer` | Transfer ownership to another active member. | **Creating a tenant** ```json Request theme={null} POST /api/tenants/ Authorization: Bearer Content-Type: application/json { "name": "Acme Corp", "slug": "acme", "plan": "enterprise", "settings": {} } ``` ```json Response — 201 Created theme={null} { "id": "b1c2d3e4-0000-0000-0000-000000000002", "name": "Acme Corp", "slug": "acme", "schema_name": "tenant_acme", "plan": "enterprise", "settings": {}, "is_active": true, "max_members": 50, "max_api_keys": 20, "owner_id": "a1b2c3d4-0000-0000-0000-000000000001", "member_count": 1, "created_at": "2026-04-09T10:00:00Z", "updated_at": "2026-04-09T10:00:00Z", "suspended_at": null } ``` The slug must be 3–50 lowercase alphanumeric characters or hyphens, starting and ending with a letter or number. Valid plans are `starter`, `growth`, and `enterprise`. **Listing tenants** ```json Request theme={null} GET /api/tenants/?plan=enterprise&is_active=true&skip=0&limit=20 Authorization: Bearer ``` ```json Response — 200 OK theme={null} { "tenants": [ { "id": "...", "name": "Acme Corp", "slug": "acme", "..." } ], "total": 1, "skip": 0, "limit": 20 } ``` ## Per-tenant configuration Each tenant record includes a `settings` field (a free-form JSON object) that you can use to store tenant-specific configuration such as allowed LLM providers, custom system prompts, feature flags, or rate limit overrides. ```json Updating tenant settings theme={null} PATCH /api/tenants/{tenant_id} Authorization: Bearer Content-Type: application/json { "settings": { "allowed_providers": ["openai"], "default_model": "gpt-4o-mini", "max_tokens_per_request": 2000, "custom_system_prompt": "You are Acme's internal assistant." } } ``` You can also update `name`, `plan`, `max_members`, and `max_api_keys` in the same call. All changes are written to the audit log automatically. ## Usage-based billing The Enterprise tier tracks resource consumption per tenant using the `UsageService`. Every AI call, RAG query, and API request records a `UsageRecord` containing a `resource_type` (such as `api_call`, `llm_token`, or `storage_mb`) and a `quantity`. You can query usage data for a tenant through two endpoints: **Aggregated summary** ```json Request theme={null} GET /api/tenants/{tenant_id}/usage/?period=30d Authorization: Bearer X-Tenant-ID: b1c2d3e4-0000-0000-0000-000000000002 ``` ```json Response — 200 OK theme={null} { "tenant_id": "b1c2d3e4-0000-0000-0000-000000000002", "period_start": "2026-03-10T10:00:00Z", "period_end": "2026-04-09T10:00:00Z", "totals": { "api_call": 4820, "llm_token": 312400, "storage_mb": 128 }, "daily_breakdown": [ { "date": "2026-04-08", "totals": { "api_call": 210, "llm_token": 14300 } }, { "date": "2026-04-09", "totals": { "api_call": 195, "llm_token": 12800 } } ] } ``` The `period` query parameter accepts `7d`, `30d`, or `90d`. **Detailed records** ```json Request theme={null} GET /api/tenants/{tenant_id}/usage/details?resource_type=llm_token&period=7d&limit=50 Authorization: Bearer ``` Usage data integrates with Stripe Metered Billing. You can push `totals` from the summary endpoint to a Stripe metered subscription item at the end of each billing period to charge tenants based on actual consumption. Both usage endpoints require the `usage:read` permission, which is granted to the `owner` and `admin` roles by default. # RAG pipeline: document ingestion and retrieval Source: https://docs.shipfastai.dev/features/rag-pipeline Ingest text or files, embed them into a vector store, and retrieve grounded answers with source citations using the built-in RAG pipeline. Retrieval-Augmented Generation (RAG) is a technique that grounds LLM responses in your own documents. Instead of relying solely on what the model learned during training, it first searches your document collection for relevant passages, then passes those passages to the LLM as context. The result is more accurate, citation-backed answers that stay within your data's scope. Shipfastai's RAG pipeline handles document ingestion, chunking, embedding, semantic search, and augmented generation — all behind a simple REST API. RAG endpoints live under `/api/rag`. The RAG pipeline requires the **Pro** or **Enterprise** tier. Basic tier accounts cannot access these endpoints. ## Ingesting documents Before you can query your documents, you need to ingest them into the vector store. Shipfastai supports two ingestion endpoints. **Ingest plain text** Send raw text content to `POST /api/rag/ingest/text`. The pipeline splits the text into overlapping chunks, embeds each chunk using OpenAI embeddings, and stores the result. Every chunk is tagged with your `user_id` for automatic isolation. ```json Request theme={null} POST /api/rag/ingest/text Authorization: Bearer Content-Type: application/json { "content": "FastAPI is a modern, fast (high-performance) web framework for building APIs with Python 3.8+ based on standard Python type hints. The key features are: fast, fast to code, fewer bugs, intuitive, easy, short, robust, and standards-based.", "metadata": { "source": "fastapi-overview", "category": "framework-docs" }, "chunk_size": 1000, "chunk_overlap": 200 } ``` ```json Response — 200 OK theme={null} { "document_ids": ["a3f1b2c4_0", "d9e8f7g6_1"], "chunks_created": 2 } ``` **Ingest a file** Upload a `.txt`, `.pdf`, or `.docx` file using a multipart `POST /api/rag/ingest/file` request. The pipeline extracts text from the file and then follows the same chunking and embedding process. ```bash cURL example theme={null} curl -X POST /api/rag/ingest/file \ -H "Authorization: Bearer " \ -F "file=@handbook.pdf" \ -F "chunk_size=1000" \ -F "chunk_overlap=200" ``` The ingestion parameters are: | Field | Type | Default | Description | | --------------- | -------- | -------- | ------------------------------------------------------- | | `content` | `string` | required | Raw text to ingest (text endpoint only). | | `metadata` | `object` | `{}` | Arbitrary key-value pairs attached to every chunk. | | `chunk_size` | `int` | `1000` | Maximum characters per chunk (100–10000). | | `chunk_overlap` | `int` | `200` | Characters of overlap between adjacent chunks (0–2000). | ## Semantic search Use `POST /api/rag/search` to find document chunks that are semantically similar to a query string, without involving the LLM. This is useful for debugging your knowledge base or building custom retrieval logic. ```json Request theme={null} POST /api/rag/search Authorization: Bearer Content-Type: application/json { "query": "What are the key features of FastAPI?", "top_k": 3, "filter": { "category": "framework-docs" } } ``` ```json Response — 200 OK theme={null} { "results": [ { "id": "a3f1b2c4_0", "content": "FastAPI is a modern, fast (high-performance) web framework...", "score": 0.94, "metadata": { "source": "fastapi-overview", "category": "framework-docs", "chunk_index": 0, "total_chunks": 2, "user_id": "a1b2c3d4-0000-0000-0000-000000000001" } } ] } ``` The `filter` field supports any metadata key-value pair you attached during ingestion. Results are automatically filtered to only include chunks belonging to your account. ## RAG queries Send a natural-language question to `POST /api/rag/query`. The pipeline embeds your question, retrieves the most relevant chunks, passes them to the LLM as context, and returns both the synthesized answer and the source documents used. **Non-streaming query** ```json Request theme={null} POST /api/rag/query Authorization: Bearer Content-Type: application/json { "question": "What makes FastAPI fast?", "top_k": 5, "min_score": 0.5, "stream": false, "chat_history": [ { "role": "user", "content": "Tell me about Python web frameworks." }, { "role": "assistant", "content": "There are many Python web frameworks..." } ], "filter": { "category": "framework-docs" } } ``` ```json Response — 200 OK theme={null} { "answer": "FastAPI achieves high performance through its use of Starlette for the web parts and Pydantic for the data parts. It is one of the fastest Python frameworks available, on par with NodeJS and Go.", "sources": [ { "id": "a3f1b2c4_0", "content": "FastAPI is a modern, fast (high-performance) web framework...", "score": 0.94, "metadata": { "source": "fastapi-overview" } } ], "usage": { "prompt_tokens": 312, "completion_tokens": 45, "total_tokens": 357 } } ``` **Streaming query** Set `stream: true` to receive the answer as a Server-Sent Event stream, identical in format to the chat streaming endpoint. Each event contains a `{ "token": "..." }` payload, and the stream ends with `data: [DONE]`. The full `RAGQueryRequest` schema: | Field | Type | Default | Description | | -------------- | -------- | -------- | ------------------------------------------------------ | | `question` | `string` | required | The natural-language question to answer. | | `top_k` | `int` | `5` | Number of document chunks to retrieve (1–50). | | `min_score` | `float` | `0.5` | Minimum similarity score to include a chunk (0.0–1.0). | | `stream` | `bool` | `false` | Stream the answer token by token. | | `chat_history` | `array` | `null` | Prior conversation turns to provide context. | | `filter` | `object` | `null` | Metadata filter applied during retrieval. | ## Vector store options The RAG pipeline uses a pluggable vector store backend. Configure which backend to use in your environment variables. FAISS is the default vector store and requires no external service. It stores all vectors in memory and optionally persists them to disk. It is ideal for local development and small-to-medium datasets. ```bash Environment theme={null} VECTOR_STORE_PROVIDER=faiss FAISS_INDEX_PATH=./data/faiss.index # optional persistence ``` No additional services are required. FAISS starts in-process alongside your FastAPI application. Pinecone is a fully managed vector database suitable for production deployments with large datasets and high query volumes. ```bash Environment theme={null} VECTOR_STORE_PROVIDER=pinecone PINECONE_API_KEY=your-pinecone-api-key PINECONE_INDEX_NAME=shipfastai-prod ``` Create your Pinecone index with a dimension that matches your embedding model (1536 for OpenAI `text-embedding-ada-002`). Chroma is an open-source, self-hosted vector database that you can run alongside your stack using Docker. ```bash Environment theme={null} VECTOR_STORE_PROVIDER=chroma CHROMA_HOST=localhost CHROMA_PORT=8001 ``` Add a `chroma` service to your `docker-compose.yml` to run it locally alongside the backend. ## Document isolation Every document chunk is stored with a `user_id` metadata field automatically set to the ID of the authenticated user who ingested it. All search and query endpoints inject a `user_id` filter into every vector store query, so users can never retrieve each other's documents — even if they use the same metadata keys. You do not need to add any `user_id` filter yourself; it is applied automatically. To delete a specific document chunk, call: ```bash theme={null} DELETE /api/rag/documents/{document_id} Authorization: Bearer ``` # Stripe billing, subscriptions, and payments Source: https://docs.shipfastai.dev/features/stripe-billing Accept payments with Stripe, manage subscriptions through the customer portal, and handle webhook events for lifecycle automation. Shipfastai includes a ready-to-use Stripe integration for subscription billing. You can create checkout sessions that take a user directly to Stripe's hosted payment page, redirect existing subscribers to the Stripe customer portal for self-service management, and receive webhook events to keep your database in sync with Stripe's state. All billing endpoints live under `/api/billing`. Shipfastai is sold as a **one-time purchase** with a **30-day money-back guarantee**. If you are not satisfied for any reason, contact support within 30 days of purchase for a full refund. ## Creating a checkout session To start a subscription, call `POST /api/billing/create-checkout-session` with the Stripe `price_id` for the plan the user selected. The endpoint creates a Stripe customer for the user if one does not already exist, then returns a `url` you should redirect the user to. ```json Request theme={null} POST /api/billing/create-checkout-session Authorization: Bearer Content-Type: application/json { "price_id": "price_1OqABCDEFGHIJKLMNOPQRSTUV" } ``` ```json Response — 200 OK theme={null} { "url": "https://checkout.stripe.com/pay/cs_test_..." } ``` Redirect the user's browser to `url`. On successful payment, Stripe redirects to `{FRONTEND_URL}/dashboard?success=true`. On cancellation it redirects to `{FRONTEND_URL}/pricing?canceled=true`. ## Accessing the customer portal Subscribers can manage their own subscription — upgrade, downgrade, or cancel — through the Stripe customer portal. Call `POST /api/billing/create-portal-session` to get a portal URL. ```json Request theme={null} POST /api/billing/create-portal-session Authorization: Bearer ``` ```json Response — 200 OK theme={null} { "url": "https://billing.stripe.com/session/sess_..." } ``` Redirect the user to that URL. When they are done, Stripe returns them to `{FRONTEND_URL}/dashboard`. ## Stripe webhooks Configure your Stripe dashboard to send webhook events to `POST /api/billing/webhook`. The endpoint verifies the `Stripe-Signature` header using your webhook secret before processing any event. The following events are handled automatically: | Event | Effect | | ------------------------------- | ----------------------------------------------------------------------------------- | | `checkout.session.completed` | Sets `subscription_status = "active"` and `subscription_tier = "pro"` for the user. | | `customer.subscription.updated` | Syncs the `subscription_status` field with the value from Stripe. | | `customer.subscription.deleted` | Sets `subscription_status = "cancelled"` and `subscription_tier = "free"`. | All other events receive a `{ "received": true }` response and are ignored. Additional event types can be handled by extending the webhook handler in your own codebase. ```json Webhook payload example theme={null} { "type": "checkout.session.completed", "data": { "object": { "metadata": { "user_id": "a1b2c3d4-..." }, "customer": "cus_..." } } } ``` ## Pricing tiers Shipfastai is available as three one-time-purchase tiers. All tiers include lifetime access to the codebase and one year of updates. Everything you need to launch a basic AI SaaS: * FastAPI + Next.js boilerplate * JWT authentication + OAuth (Google, GitHub) * Stripe subscriptions * Email integration (Brevo) * Docker Compose setup * Railway + Vercel deployment * CI/CD pipeline (GitHub Actions) * 1 year of updates [Get started →](/quickstart) For developers building AI-powered products. Includes everything in Basic, plus: * RAG pipeline (LangChain + FAISS) * Streaming LLM chat * AI usage metering * Admin dashboard * VectorDB abstraction (FAISS / Pinecone / Chroma) * LangSmith tracing * Pinecone integration [Get started →](/quickstart) For teams building production AI platforms. Includes everything in Pro, plus: * QLoRA fine-tuning scripts (PyTorch + PEFT) * Multi-tenancy with tenant isolation * Usage-based billing (Stripe metered) * Priority support * HuggingFace Transformers integration * Custom deployment support * Architecture consultation call [Get started →](/quickstart) # Add a new LLM provider to your AI app Source: https://docs.shipfastai.dev/guides/adding-llm-provider Configure OpenAI, Anthropic, or Google Gemini in Shipfastai, switch providers per request, and extend the LLM abstraction to add a custom provider. Shipfastai's LLM abstraction layer lets you swap providers — OpenAI, Anthropic, or Google Gemini — by changing a single `provider` field in your API request. Your application code never needs to know which underlying SDK is in use. The `get_llm_provider` factory in `app/packages/ai/llm.py` instantiates the right client, passes the correct API key from your environment, and returns a consistent `ChatResponse` regardless of which model generated it. ## Supported providers Shipfastai ships with three first-class provider implementations inside `products/pro/backend/app/packages/ai/llm.py`: | Provider | `provider` value | Default model | Env var | | ------------- | ---------------- | -------------------------- | ------------------- | | OpenAI | `openai` | `gpt-4o` | `OPENAI_API_KEY` | | Anthropic | `anthropic` | `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` | | Google Gemini | `gemini` | `gemini-2.0-flash` | `GOOGLE_API_KEY` | All three implement the same abstract `LLMProvider` interface with `chat()` and `stream_chat()` methods, so switching providers requires no changes to your route handlers. ## Configuring OpenAI Add your OpenAI API key to your backend `.env` file: ```bash .env theme={null} OPENAI_API_KEY=sk-... ``` Pass `"provider": "openai"` in your request body. You can optionally specify a `model`; if you omit it the default `gpt-4o` is used. ```json POST /api/ai/chat theme={null} { "provider": "openai", "model": "gpt-4o", "messages": [ { "role": "user", "content": "Explain RAG in one sentence." } ], "temperature": 0.7, "max_tokens": 256 } ``` ## Configuring Anthropic ```bash .env theme={null} ANTHROPIC_API_KEY=sk-ant-... ``` ```json POST /api/ai/chat theme={null} { "provider": "anthropic", "model": "claude-opus-4-5", "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "Explain RAG in one sentence." } ], "temperature": 0.7, "max_tokens": 256 } ``` The `AnthropicProvider` automatically extracts `system`-role messages and passes them to Anthropic's `system` parameter, so your request format is identical across providers. ## Configuring Google Gemini ```bash .env theme={null} GOOGLE_API_KEY=AIza... ``` ```json POST /api/ai/chat theme={null} { "provider": "gemini", "model": "gemini-2.0-flash", "messages": [ { "role": "user", "content": "Explain RAG in one sentence." } ], "temperature": 0.7, "max_tokens": 256 } ``` ## Switching providers at runtime Because the `provider` and `model` fields are part of each request body, you can switch providers on a per-request basis without redeploying. This is useful for A/B testing models or falling back to a cheaper provider under load. ```json POST /api/ai/chat theme={null} { "provider": "openai", "model": "gpt-4o", "messages": [{ "role": "user", "content": "Hello!" }] } ``` ```json POST /api/ai/chat theme={null} { "provider": "anthropic", "model": "claude-sonnet-4-20250514", "messages": [{ "role": "user", "content": "Hello!" }] } ``` ```json POST /api/ai/chat theme={null} { "provider": "gemini", "model": "gemini-2.0-flash", "messages": [{ "role": "user", "content": "Hello!" }] } ``` You can also enable streaming for any provider by adding `"stream": true` to the request body. The endpoint returns a `text/event-stream` response where each event is a JSON object `{ "token": "..." }`, terminated by `data: [DONE]`. ```json POST /api/ai/chat (streaming) theme={null} { "provider": "openai", "model": "gpt-4o", "messages": [{ "role": "user", "content": "Write a haiku about Python." }], "stream": true } ``` ## Extending with a new provider All providers inherit from the abstract base class `LLMProvider` defined in `products/pro/backend/app/packages/ai/llm.py`. To add a new provider, you implement two async methods and register the provider in the factory function. Add a new class that extends `LLMProvider` and implements `chat()` and `stream_chat()`: ```python products/pro/backend/app/packages/ai/llm.py theme={null} class GroqProvider(LLMProvider): """Groq LLM provider.""" def __init__(self, api_key: Optional[str] = None, model: str = "llama-3.3-70b-versatile"): from groq import AsyncGroq self.client = AsyncGroq(api_key=api_key or os.getenv("GROQ_API_KEY")) self.model = model async def chat( self, messages: list[Message], temperature: float = 0.7, max_tokens: int = 1000, ) -> ChatResponse: response = await self.client.chat.completions.create( model=self.model, messages=[m.model_dump() for m in messages], temperature=temperature, max_tokens=max_tokens, ) return ChatResponse( content=response.choices[0].message.content or "", model=response.model, usage={ "prompt_tokens": response.usage.prompt_tokens, "completion_tokens": response.usage.completion_tokens, "total_tokens": response.usage.total_tokens, }, finish_reason=response.choices[0].finish_reason, ) async def stream_chat( self, messages: list[Message], temperature: float = 0.7, max_tokens: int = 1000, ) -> AsyncGenerator[str, None]: stream = await self.client.chat.completions.create( model=self.model, messages=[m.model_dump() for m in messages], temperature=temperature, max_tokens=max_tokens, stream=True, ) async for chunk in stream: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content ``` Update `get_llm_provider()` to handle your new provider string: ```python products/pro/backend/app/packages/ai/llm.py theme={null} def get_llm_provider( provider: Literal["openai", "anthropic", "gemini", "groq"] = "openai", model: Optional[str] = None, ) -> LLMProvider: if provider == "openai": return OpenAIProvider(model=model or "gpt-4o") elif provider == "anthropic": return AnthropicProvider(model=model or "claude-sonnet-4-20250514") elif provider == "gemini": return GeminiProvider(model=model or "gemini-2.0-flash") elif provider == "groq": return GroqProvider(model=model or "llama-3.3-70b-versatile") else: raise ValueError(f"Unknown provider: {provider}") ``` Add `GroqProvider` to the `__all__` list in `packages/ai/__init__.py`, then add `GROQ_API_KEY` to your `.env` file. Because the chat endpoint in `app/api/ai/chat.py` delegates entirely to `get_llm_provider()`, your new provider is immediately available to all routes — including streaming completions — without any further changes. # Customize the Shipfastai frontend and branding Source: https://docs.shipfastai.dev/guides/customizing-ui Tailor the Next.js frontend to match your brand — update colors, fonts, logo, and page layouts using Tailwind CSS and shadcn/ui. Shipfastai's frontend is a Next.js 14 App Router application built with Tailwind CSS and shadcn/ui. Every visual aspect — the color palette, typography, logo, and page layouts — is designed to be overridden without touching the core business logic. The steps below walk you through the most common customizations, from swapping your brand name to adding entirely new pages. ## Updating branding The app name appears in browser tabs, Open Graph tags, and the default metadata defined in `src/app/layout.tsx`. Update it in two places: **1. Metadata in `layout.tsx`** ```typescript src/app/layout.tsx theme={null} export const metadata: Metadata = { title: { default: 'Your App Name - AI SaaS', template: '%s | Your App Name', }, description: 'Your app description here.', }; ``` **2. Logo and favicon** Place your logo files inside the `public/` directory at the project root. Reference them in your header component using Next.js's `Image` component: ```typescript src/components/shared/logo.tsx theme={null} import Image from 'next/image'; export function Logo() { return Your App Name; } ``` Replace `public/favicon.ico` with your own favicon. For broader browser and PWA support, also add `public/apple-touch-icon.png` (180×180 px) and `public/icon-192.png`. ## Colors and theme Shipfastai uses CSS custom properties to define its color system, which Tailwind maps through `tailwind.config.ts`. You change the entire color palette by editing the variable values in `src/app/globals.css` — you never need to modify `tailwind.config.ts` itself. The light and dark theme values are defined under `:root` and `.dark` respectively: ```css src/app/globals.css theme={null} @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --accent: 210 40% 96.1%; --radius: 0.5rem; /* ... other tokens */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --primary: 210 40% 98%; /* ... other tokens */ } } ``` All values use the HSL space format (`H S% L%`) without the `hsl()` wrapper, which lets Tailwind compose opacity variants automatically. To change your primary brand color, update `--primary` (and `--primary-foreground` for text on primary backgrounds) in both `:root` and `.dark`. Use Tailwind's `dark:` prefix in your components to apply dark-mode-specific utility classes without needing additional CSS. For example, `className="text-gray-900 dark:text-gray-100"` automatically responds to the active theme class on ``. ## shadcn/ui components Shipfastai uses shadcn/ui with the `new-york` style and `neutral` base color, configured in `components.json` at the project root: ```json components.json theme={null} { "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "css": "src/app/globals.css", "baseColor": "neutral", "cssVariables": true }, "aliases": { "components": "@/components", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" } } ``` To add a new shadcn/ui component, run the CLI from your frontend directory: ```bash theme={null} npx shadcn@latest add ``` For example, to add the `dialog` component: ```bash theme={null} npx shadcn@latest add dialog ``` This writes the component source to `src/components/ui/dialog.tsx`, which you can then import and customize freely. Because the components are copied into your repository, you own the code and can modify them without overriding a package. ## Modifying pages The frontend follows the Next.js App Router convention. All user-facing routes live under `src/app/`, grouped into three route groups: | Route group | Path | Purpose | | ------------- | ------------------------------------- | ----------------------- | | `(marketing)` | `/`, `/pricing` | Public landing pages | | `(dashboard)` | `/dashboard`, `/billing`, `/settings` | Authenticated app pages | | `(auth)` | `/login`, `/register` | Authentication flows | To edit the landing page, open `src/app/(marketing)/page.tsx`. The hero section, features grid, and CTA section are each a self-contained JSX block you can rearrange or replace: ```typescript src/app/(marketing)/page.tsx theme={null} export default function HomePage() { return ( <> {/* Hero — edit headline and description here */}

Your new headline goes here

{/* Features grid */}
{/* Add or remove entries */}
); } ``` To edit the dashboard, open `src/app/(dashboard)/dashboard/page.tsx`. The stat cards, recent activity feed, and quick-action buttons are all rendered inline and easy to replace with your own data. ## Adding new pages To add a new public page, create a `page.tsx` file inside `src/app/(marketing)/`: ```bash theme={null} # Creates the route /about touch src/app/(marketing)/about/page.tsx ``` ```typescript src/app/(marketing)/about/page.tsx theme={null} export default function AboutPage() { return (

About us

Your content here.

); } ``` To add a new authenticated app page, create the file inside `src/app/(dashboard)/`: ```bash theme={null} # Creates the route /analytics touch src/app/(dashboard)/analytics/page.tsx ``` The `(dashboard)` layout in `src/app/(dashboard)/layout.tsx` automatically wraps the page with the sidebar and authentication guard, so you do not need to add those yourself. ## Dark mode Dark mode is pre-configured using the `class` strategy in `tailwind.config.ts`. When the `dark` class is present on the `` element, Tailwind applies your `.dark` CSS variables and any `dark:` utility overrides in your components. Shipfastai's `providers.tsx` wraps the app in a `QueryClientProvider`, and you can add `next-themes` to manage theme toggling: ```bash theme={null} npm install next-themes ``` ```typescript src/app/providers.tsx theme={null} 'use client'; import { ThemeProvider } from 'next-themes'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()); return ( {children} ); } ``` Then add a toggle button anywhere in your UI using the `useTheme` hook: ```typescript src/components/shared/theme-toggle.tsx theme={null} 'use client'; import { useTheme } from 'next-themes'; import { Button } from '@/components/ui/button'; export function ThemeToggle() { const { theme, setTheme } = useTheme(); return ( ); } ``` # Fine-tune a custom LLM with QLoRA Source: https://docs.shipfastai.dev/guides/fine-tuning Use Shipfastai's included QLoRA fine-tuning scripts with PyTorch and PEFT to train a custom LLM on your own dataset efficiently. Shipfastai's Enterprise tier includes a complete QLoRA fine-tuning pipeline under `products/enterprise/scripts/finetune/`. QLoRA (Quantized Low-Rank Adaptation) lets you train a large language model on consumer or mid-range cloud GPUs by loading the base model in 4-bit precision and training only a small set of adapter weights. When training is done, you merge those adapters back into the base model and deploy the result as a standard HuggingFace model — which the built-in `GeminiProvider` or HuggingFace inference endpoints can then serve. The fine-tuning scripts are only available in the **Enterprise tier**. Upgrade your license before following the steps below. ## Prerequisites Before running any training, make sure you have the following in place. A 7B parameter model requires roughly 10–14 GB of VRAM in 4-bit mode. An A100 40 GB, RTX 3090, or RTX 4090 all work well. Smaller models (1B–3B) fit on an RTX 3080. The scripts use Python 3.11 type annotations. Check your version with `python --version` and upgrade if needed. If you do not have a suitable local GPU, cloud GPU providers like [RunPod](https://runpod.io) and [Lambda Labs](https://lambdalabs.com) offer hourly instances with A100s and H100s. Mount your dataset and output directory from persistent storage so checkpoints survive instance restarts. Install the Enterprise dependencies alongside the base and Pro requirements: ```bash theme={null} pip install -r products/enterprise/requirements-enterprise.txt ``` This installs the following key packages: | Package | Purpose | | ------------------------- | -------------------------------------- | | `transformers>=4.37.0` | Model loading and tokenization | | `peft>=0.8.0` | LoRA adapter training with PEFT | | `bitsandbytes>=0.42.0` | 4-bit quantization | | `datasets>=2.16.0` | Dataset loading and preprocessing | | `accelerate>=0.26.0` | Multi-GPU and mixed-precision training | | `trl>=0.7.10` | Supervised fine-tuning utilities | | `huggingface-hub>=0.20.0` | Pushing merged models to the Hub | ## Preparing your dataset The training script expects a JSONL file where each line is a JSON object with a `messages` key containing a list of chat turns. This is the standard chat-template format used by most instruction-tuned models: ```jsonl data/train.jsonl theme={null} {"messages": [{"role": "system", "content": "You are a customer support agent."}, {"role": "user", "content": "How do I cancel my subscription?"}, {"role": "assistant", "content": "You can cancel your subscription from the Billing page in your dashboard."}]} {"messages": [{"role": "user", "content": "What payment methods do you accept?"}, {"role": "assistant", "content": "We accept all major credit cards, PayPal, and bank transfers."}]} ``` If your data is in a different format — for example a JSON array with `instruction`, `input`, and `output` fields — use the `prepare_data.py` script to convert and split it: ```bash theme={null} python products/enterprise/scripts/finetune/prepare_data.py \ --input data/raw.json \ --output-dir data/processed/ \ --instruction-key instruction \ --input-key input \ --output-key output \ --system-prompt "You are a helpful assistant." \ --train-ratio 0.9 ``` This produces `data/processed/train.jsonl` (90%) and `data/processed/val.jsonl` (10%), both in the `messages` chat format. ## Running QLoRA training Run `qlora_train.py` with your dataset and chosen base model. The default base model is `mistralai/Mistral-7B-v0.1`, but any HuggingFace causal LM works. ```bash theme={null} python products/enterprise/scripts/finetune/qlora_train.py \ --model-name mistralai/Mistral-7B-v0.1 \ --train-file data/processed/train.jsonl \ --val-file data/processed/val.jsonl \ --output-dir outputs/my-model-adapter \ --num-epochs 3 \ --batch-size 4 \ --lora-r 64 \ --lora-alpha 16 \ --learning-rate 2e-4 ``` Key hyperparameters: | Flag | Default | Description | | ----------------- | --------------------------- | ----------------------------------------------------------------------- | | `--model-name` | `mistralai/Mistral-7B-v0.1` | HuggingFace model ID or local path | | `--num-epochs` | `3` | Number of full passes over the training set | | `--batch-size` | `4` | Per-device training batch size | | `--lora-r` | `64` | LoRA rank — higher values capture more adaptation at the cost of memory | | `--lora-alpha` | `16` | LoRA scaling factor | | `--lora-dropout` | `0.1` | Dropout applied to LoRA layers | | `--learning-rate` | `2e-4` | AdamW learning rate | | `--max-length` | `2048` | Maximum token length per example | The script saves checkpoints to `--output-dir` every 100 steps (configurable with `--save-steps`) and keeps the last three. Training logs are printed to stdout. To enable Flash Attention 2 for faster training on supported GPUs (A100, H100): ```bash theme={null} pip install flash-attn --no-build-isolation python products/enterprise/scripts/finetune/qlora_train.py \ --use-flash-attention \ # ... other flags ``` ## Merging LoRA adapters After training, the `outputs/my-model-adapter/` directory contains only the small adapter weights, not a standalone model. Use `merge_adapter.py` to merge the adapters back into the base model weights: ```bash theme={null} python products/enterprise/scripts/finetune/merge_adapter.py \ --base-model mistralai/Mistral-7B-v0.1 \ --adapter-path outputs/my-model-adapter \ --output-path outputs/my-model-merged ``` The merged model is saved to `outputs/my-model-merged/` as a standard HuggingFace `AutoModelForCausalLM` — no PEFT dependency required at inference time. To publish the merged model directly to the HuggingFace Hub: ```bash theme={null} python products/enterprise/scripts/finetune/merge_adapter.py \ --base-model mistralai/Mistral-7B-v0.1 \ --adapter-path outputs/my-model-adapter \ --output-path outputs/my-model-merged \ --push-to-hub \ --hub-repo-id your-username/my-fine-tuned-model ``` Make sure you are authenticated with `huggingface-cli login` before pushing. ## Using your fine-tuned model Once your model is available — either locally or on the HuggingFace Hub — you can serve it through Shipfastai's existing chat endpoint using a HuggingFace inference endpoint or a local `vllm` / `text-generation-inference` server. Point the AI chat API at your model by setting the `model` field in your request. If you are running a local inference server that exposes an OpenAI-compatible API, use the `openai` provider and override the base URL via an environment variable or by extending `OpenAIProvider`: ```json POST /api/ai/chat theme={null} { "provider": "openai", "model": "your-username/my-fine-tuned-model", "messages": [ { "role": "user", "content": "How do I cancel my subscription?" } ] } ``` For HuggingFace Inference Endpoints, use the endpoint URL as the `OPENAI_API_KEY` base URL and set the `model` to your repository ID. Refer to the [Add LLM Provider guide](/guides/adding-llm-provider) for instructions on creating a custom provider class if you need a dedicated integration. # What is Shipfastai? Plans, stack, and prerequisites Source: https://docs.shipfastai.dev/introduction Shipfastai is a production-ready AI SaaS boilerplate for Python developers. Learn what each pricing tier includes and what you need before getting started. Shipfastai is an opinionated SaaS boilerplate built for Python developers who want to launch AI-powered products without spending weeks on infrastructure. It gives you a production-ready FastAPI backend, a Next.js 15 frontend, JWT authentication, Stripe billing, LangChain RAG pipelines, and multi-provider LLM support — all pre-wired and ready for you to build on top of. ## What you get Shipfastai is sold as a one-time purchase in three tiers. Each tier is a complete, runnable codebase — not a template with stubs. FastAPI + Next.js boilerplate with JWT authentication, OAuth (Google and GitHub), Stripe subscriptions, Brevo email integration, Docker Compose, Railway + Vercel deployment, and a GitHub Actions CI/CD pipeline. Includes one year of updates. Everything in Basic, plus a LangChain RAG pipeline with FAISS/Pinecone/Chroma abstraction, streaming LLM chat, AI usage metering, an admin dashboard, and LangSmith tracing. Everything in Pro, plus QLoRA fine-tuning scripts (PyTorch + PEFT), multi-tenancy with tenant isolation, usage-based billing via Stripe Metered, HuggingFace Transformers integration, priority support, and an architecture consultation call. All tiers share the same FastAPI + Next.js foundation, authentication, Stripe billing, Docker setup, and CI/CD pipeline. Higher tiers layer on AI capabilities and team features. ## Tech stack Shipfastai is built on widely adopted, production-proven tools: | Layer | Technology | | -------------- | ------------------------- | | Backend | FastAPI (Python 3.11+) | | Frontend | Next.js 15 (App Router) | | Database | PostgreSQL via SQLAlchemy | | Cache | Redis | | AI / LLM | LangChain, OpenAI | | Payments | Stripe | | Infrastructure | Docker, Terraform | | CI/CD | GitHub Actions | ## Prerequisites Before you set up Shipfastai, make sure you have the following installed on your machine: * **Docker & Docker Compose** — required to run the full stack locally * **Node.js 18+** — required for the Next.js frontend * **Python 3.11+** — required if you run the backend outside Docker * **pnpm** — recommended package manager for the frontend (`npm install -g pnpm`) Shipfastai is a one-time purchase with lifetime access to the code. You receive one year of updates after purchase. If you are not satisfied within the first 30 days, you are eligible for a full refund — no questions asked. # Project structure and codebase overview Source: https://docs.shipfastai.dev/project-structure A guided tour of the Shipfastai directory layout — what lives where, how the backend and frontend are organized, and how tiers extend the structure. Shipfastai separates the backend and frontend into sibling directories under the project root. The backend is a standalone FastAPI application and the frontend is a Next.js 15 app — each has its own dependency file, environment configuration, and Dockerfile. Docker Compose ties them together for local development and production. ## Directory tree ``` ├── backend/ │ ├── app/ │ │ ├── api/ # API route handlers grouped by feature │ │ │ ├── auth/ # Authentication endpoints (login, register, OAuth) │ │ │ ├── users/ # User profile and account management │ │ │ └── billing/ # Stripe webhook handlers and subscription logic │ │ ├── core/ # Configuration, security, and database utilities │ │ ├── models/ # Database models │ │ └── schemas/ # Pydantic request and response schemas │ ├── alembic/ # Database migration scripts │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── app/ # Next.js App Router pages and layouts │ │ ├── components/ # Reusable React components │ │ ├── hooks/ # Custom React hooks │ │ └── lib/ # Shared utilities and API client helpers │ └── package.json ├── docker-compose.yml └── .env.example ``` ## Backend directories **`backend/app/api/`** contains all API route handlers, organized into subdirectories by feature. Each subdirectory covers one feature area (auth, users, billing, and so on). When you add a new feature to your app, create a new subdirectory here with its own route definitions. **`backend/app/core/`** holds shared utilities that every route depends on: application settings loaded from environment variables, JWT token creation and verification, password hashing, and database session management. **`backend/app/models/`** defines the data models that map to database tables. When you add a new model, run the migration command from the `backend/` directory to apply the schema change to your database. **`backend/app/schemas/`** contains the Pydantic models that define the shape of incoming request bodies and outgoing response payloads. These control exactly what data is exposed through the API. ## Frontend directories **`frontend/src/app/`** follows the Next.js App Router convention. Each subdirectory represents a route segment and contains a `page.tsx` for the page component and optionally a `layout.tsx`, `loading.tsx`, or `error.tsx`. Marketing pages live under `(marketing)/` and authenticated app pages under `(app)/`. **`frontend/src/components/`** contains reusable UI components shared across pages. Components are organized by type — `ui/` for primitive elements (buttons, inputs, cards) and feature-level components in their own directories. ## How tiers extend the structure The Basic tier contains the foundation. Higher tiers add directories to the same layout without removing anything — you can upgrade by merging the additional directories into your existing project. **Pro tier** adds the following directories to `backend/app/api/`: * `api/ai/` — Streaming LLM chat endpoints * `api/rag/` — Document ingestion and retrieval endpoints * `api/api_keys/` — API key management for AI usage metering **Enterprise tier** adds: * `api/tenants/` — Multi-tenant management endpoints with isolation logic * `scripts/finetune/` — QLoRA fine-tuning scripts using PyTorch and PEFT If you purchased Basic and want to upgrade to Pro or Enterprise, contact support. You pay only the difference between tiers and receive access to the additional directories to merge into your existing codebase. # Get started with Shipfastai in minutes Source: https://docs.shipfastai.dev/quickstart Download your licensed product, configure your environment variables, and run the full Shipfastai stack locally using Docker Compose in under 10 minutes. This guide walks you through getting Shipfastai running on your machine for the first time. By the end, you will have the full stack running locally — FastAPI backend on port 8000, Next.js frontend on port 3000, PostgreSQL via Supabase, and Redis — all orchestrated by Docker Compose. After purchasing a license from [shipfastai.dev](https://shipfastai.dev), log into your [Dashboard](https://shipfastai.dev/dashboard) to download your product: 1. Go to **Dashboard** → **Your Products** 2. Find your purchased tier (Basic, Pro, or Enterprise) 3. Click **Download ZIP** — the file is password-protected with your license key 4. Extract the ZIP using your license key as the password ```bash theme={null} # Navigate to your extracted product folder cd shipfastai-basic # or shipfastai-pro, shipfastai-enterprise ``` Copy the example environment file and fill in your credentials: ```bash theme={null} cp .env.example .env ``` Open `.env` and set the following required variables before starting the app: | Variable | Description | | ------------------------------- | ------------------------------------------------------------ | | `NEXT_PUBLIC_SUPABASE_URL` | Your Supabase project URL | | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Your Supabase anonymous/public key | | `SUPABASE_SERVICE_ROLE_KEY` | Your Supabase service role key (keep secret!) | | `STRIPE_SECRET_KEY` | Your Stripe secret key (`sk_test_...` for development) | | `STRIPE_WEBHOOK_SECRET` | Your Stripe webhook signing secret (`whsec_...`) | | `STRIPE_BASIC_PRICE_ID` | Stripe price ID for Basic tier | | `STRIPE_PRO_PRICE_ID` | Stripe price ID for Pro tier | | `STRIPE_ENTERPRISE_PRICE_ID` | Stripe price ID for Enterprise tier | | `BREVO_API_KEY` | API key from [Brevo](https://brevo.com) for marketing emails | | `NEXT_PUBLIC_APP_URL` | Your app URL (e.g., `http://localhost:3000`) | The Supabase variables in `.env.example` connect to your Supabase project. Create a free project at [supabase.com](https://supabase.com) if you haven't already. ```bash theme={null} # Supabase configuration NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-service-role-key ``` Start the full stack in the background with a single command: ```bash theme={null} docker-compose up -d ``` Docker Compose starts the services: the FastAPI backend, the Next.js frontend, and Redis. On first run it will build the images, which takes a few minutes. Subsequent starts are fast. To watch the logs: ```bash theme={null} docker-compose logs -f ``` To stop all services: ```bash theme={null} docker-compose down ``` Once all containers are running, open the following URLs in your browser: | Service | URL | | ---------------------------- | ---------------------------------------------------------- | | Frontend | [http://localhost:3000](http://localhost:3000) | | Backend API | [http://localhost:8000](http://localhost:8000) | | Interactive API docs | [http://localhost:8000/docs](http://localhost:8000/docs) | | Alternative API docs (ReDoc) | [http://localhost:8000/redoc](http://localhost:8000/redoc) | The interactive API docs at `/docs` are generated automatically by FastAPI from your route definitions. Use them to test endpoints directly from the browser without any additional tooling. To run the stack without Docker, start the backend and frontend in separate terminals: ```bash theme={null} # Terminal 1 — backend cd backend python -m venv venv source venv/bin/activate # on Windows: venv\Scripts\activate pip install -r requirements.txt uvicorn app.main:app --reload --port 8000 ``` ```bash theme={null} # Terminal 2 — frontend cd frontend pnpm install pnpm dev ``` You'll need a running Supabase project for the database. The free tier at [supabase.com](https://supabase.com) is sufficient for local development. The variables above cover the minimum required to start the app. For a full reference of every environment variable — including optional settings for Redis, email configuration, and deployment settings — see the [Configuration](/configuration/environment-variables) section.