Skip to content

Latest commit

 

History

History
241 lines (175 loc) · 14.3 KB

File metadata and controls

241 lines (175 loc) · 14.3 KB

Development Instructions for LLM agents

This document provides essential guidance for AI agents contributing to the Polar codebase. Imagine this file as a new joiner to the team who needs to understand the coding standards, practices, and conventions used in this repository.

General Guidelines

  • Do not add comments to the code unless necessary. The code should be self-explanatory.
  • Use meaningful variable and function names.
  • Follow good practices and code conventions.
  • Make sure that all the new code is maintanable and follows the SOLID principles.
  • Do not modify unrelated code to the task or issue you are working on.

Architecture Overview

Polar is a payment infrastructure platform with a monorepo structure.

  • server/: The backend is a Python application built with the FastAPI framework.
    • Database: It uses PostgreSQL as its database, with SQLAlchemy as the ORM. Database models are located in server/polar/models.
    • Background Jobs: Asynchronous tasks are handled by Dramatiq workers.
    • API: The core API logic is in server/polar/, with routes organized into modules.
  • clients/: The frontend applications are managed with Turborepo and pnpm.
    • clients/apps/web/: The main web dashboard application built with Next.js.
    • clients/apps/app/: iOS and Android app built with Expo and React Native.
    • clients/packages/ui/: A shared library of React components built with Radix UI and Tailwind CSS.
    • clients/packages/client/: The generated API client and data-fetching hooks.
  • dev/: Contains scripts and tools for development.
  • docs/: Contains the documentation of the service, intended as a reference for developers and users. It's generated by Mintlify: https://mintlify.com/docs/llms.txt
  • sdk/overlay: Our official SDK are generated with Speakeasy, a tool for generating SDKs from OpenAPI specifications. We sometimes need to tweak our schema to ensure the generated SDKs work correctly. This directory contains these tweaks, which follow the OpenAPI Overlay specification: https://spec.openapis.org/overlay/latest.html

Development Workflow

The primary setup and workflow instructions are in DEVELOPMENT.md.

Running the Application

  • Backend: In the server/ directory, run the following commands in separate terminals:
    • uv run task api to start the FastAPI server (http://127.0.0.1:8000).
    • uv run task worker to start the Dramatiq background worker.
  • Frontend: In the clients/ directory, run pnpm run dev to start the Next.js development server (http://127.0.0.1:3000).

Database Migrations

The project uses Alembic for database migrations, located in server/migrations/. To apply migrations, run uv run task db_migrate from the server/ directory. When creating a new model, you'll need to generate a new migration script.

A migration script can be generated automatically from the models changes using the following command, which should be run from the server/ directory:

uv run alembic revision --autogenerate -m "<description>"

If you need to create an empty migration script, which can be useful if we need to perform data migrations, use:

uv run alembic revision -m "<description>"

Linting and testing

The backend requires to be linted and type-checked. To do so, run:

uv run task lint && uv run task lint_types

Backend tests are located in the tests/ directory. It uses pytest for testing. To run the tests, use:

uv run task test

To run a specific test file:

uv run pytest tests/path/to/test_file.py

To run a specific test class or method, use the :: syntax:

uv run pytest tests/path/to/test_file.py::TestClassName::test_method_name

Use uv run for Python Commands

CRITICAL: Always prefix Python commands with uv run when working in the Polar environment. This ensures:

  • The correct Python version (3.14) is used
  • All project dependencies are available
  • Environment variables are properly loaded
  • Commands run in the correct virtual environment context

Backend Conventions

  • Modular Structure: The code is organized in a modular way, with each module in its own folder under server/polar/. A typical module contains:
    • endpoints.py: API endpoints.
    • service.py: Business logic, encapsulated in service classes.
    • schemas.py: Pydantic schemas for API request/response validation and serialization.
    • repository.py: Database query logic, using SQLAlchemy.
  • Models: Note that SQLAlchemy models are an exception to the modular structure and are defined globally in server/polar/models.
  • API Client Generation: The frontend's TypeScript client is generated from the backend's OpenAPI schema. After making changes to the API, you may need to run pnpm run generate in clients/packages/client to update the client.

Import Organization

  • imports at module top: Import statements like from sqlalchemy import update, select should be at the top of the file, not inside methods or functions.
  • Follow modular structure: Import models from polar.models, services from their respective modules, following the established patterns.
  • Dependency injection: Use FastAPI's dependency injection system for repositories and services.

Repository Layer Standards

  • Accept domain objects over IDs: Repository methods should prefer accepting domain objects (e.g., Order) instead of just UUIDs when the calling code already has the object.

    # Preferred
    async def release_payment_lock(self, order: Order, *, flush: bool = False) -> Order:
        return await self.update(order, update_dict={"payment_lock_acquired_at": None}, flush=flush)
    
    # Avoid when object is available
    async def release_payment_lock(self, order_id: UUID) -> None:
        # ...
  • Return updated objects: Repository methods should return updated domain objects to provide the caller with the latest state.

  • Use flush parameters: Include flush: bool = False parameters for controlling transaction boundaries. It should always be a keyword argument, after * to avoid confusion.

  • Inherit from RepositoryBase: Follow the established repository inheritance patterns.

Service Layer Standards

  • Proper exception handling: Use appropriate HTTP status codes in custom exceptions:
    • 409 for conflicts (e.g., PaymentAlreadyInProgress)
    • 422 for validation errors
    • 404 for not found errors
    class PaymentAlreadyInProgress(OrderError):
        def __init__(self, order: Order) -> None:
            self.order = order
            message = f"Payment for order {order.id} is already in progress"
            super().__init__(message, 409)  # Include status code
  • Meaningful error messages: Include relevant entity IDs and context in error messages for debugging.
  • Async patterns: Use proper async/await patterns with SQLAlchemy sessions.
  • Session management: Use dependency injection for database sessions, don't create sessions manually.

Exception Handling Standards

  • Inherit from appropriate base classes: Custom exceptions should inherit from domain-specific error classes (e.g., OrderError).
  • Include HTTP status codes: Pass status codes as the second parameter to the parent constructor for known errors.
  • Contextual information: Store relevant domain objects as attributes for error handling and logging.

SQLAlchemy & Database Standards

  • Use ORM patterns consistently: Prefer SQLAlchemy ORM methods over raw SQL.
  • Session management: Use AsyncSession with proper dependency injection.
  • Repository patterns: Encapsulate database logic in repository classes inheriting from RepositoryBase.

In most cases, you should never call session.commit() directly in business logic. We have established patterns for that: the API backend automatically commits the session at the end of each request, and background workers commit the session at the end of each task. It avoids to have a database in an inconsistent state in case of exceptions. If you have a session.commit() in your code, it's likely a mistake. Otherwise, please explicitly document why it's necessary.

If you need to ensure that data is flushed to the database, to run constraints or fill server defaults, use session.flush() instead. Bear in mind though that it might not be necessary, as SQLAlchemy automatically flushes pending changes before read operations.

Testing Standards

  • Test file structure: Test files mirror the source code structure. If you have server/polar/foo/endpoints.py, the corresponding tests will be in tests/foo/test_endpoints.py.
  • Avoid redundant fixture setup: Don't manually set data that fixtures already provide (e.g., customer.stripe_customer_id when the customer fixture includes it).
  • Descriptive test names: Use method names that clearly describe the behavior being tested.
  • Encapsulate test logic: Use class based tests. Usually we have one class per method that we want to test, and each test case is a different scenario for that method.
  • test_task and test_endpoints: are an E2E test where mocking is not used. They should be used to test the actual behavior of the application, including database interactions and external services if possible.
  • Proper mocking: Mock external services (Stripe, etc.) using the established patterns with MagicMock.
  • Use existing fixtures: Leverage SaveFixture, AsyncSession, and other established test utilities.
  • Test structure: Follow the existing patterns with pytest.mark.asyncio and class-based test organization.

Tax ID Validation

When adding or modifying tax ID validators in server/polar/tax/tax_id.py:

  • Keep validators minimal: Do not add lengthy docstrings to validator classes. The code should be self-explanatory.
  • Follow existing patterns: Use the same structure as other validators (e.g., CLTINValidator, TRTINValidator).
  • Use stdnum library: Leverage the stdnum library for validation when a module exists for the tax ID type.
  • Minimal tests: Add a few representative valid format tests and only one invalid test case per tax ID type. Do not add excessive negative tests.

Internationalization (i18n)

Translation files are located in clients/packages/i18n/src/locales/. When adding new translatable strings, only add them to en.ts. Do not manually edit any other locale files (e.g., fr.ts, de.ts, sv.ts). A CI job automatically translates new English strings into all supported languages and commits the results to the branch. After pushing changes to en.ts, pull the branch once the CI translation job completes.

Frontend Conventions

  • Modular Structure: The code is organized in a modular way, with features grouped into their own folders.
  • Key Page Directories:
    • apps/web/src/app/(main)/dashboard: The main dashboard for logged-in users.
    • apps/web/src/app/(main)/[organization]: Organization-specific pages.
  • Data Fetching: The frontend uses TanStack Query for data fetching. Hooks are generated by openapi-typescript-codegen and are available from the @polar-sh/sdk package.
  • State Management: Global state is managed with Zustand.
  • UI Components: Use components from the shared clients/packages/ui library whenever possible. These components are built on top of Tailwind CSS and Radix UI.
  • Styling: Use Tailwind CSS for styling.
  • Best Practices: The code follows React and Next.js best practices.

Authentication

The backend uses a custom authentication system built on FastAPI's dependency injection.

  • AuthSubject: The core of the system is the AuthSubject[T] type, which represents the authenticated entity. T can be User, Organization, Customer, or Anonymous. It's available in endpoint signatures as a dependency. If an endpoint does not have an auth_subject dependency, it is public and accessible to anonymous users.

  • Module-Specific Authentication: For most API, authentication models should be defined within each module's auth.py file. This allows creating authenticators with specific scopes and allowed subjects.

    # server/polar/discount/auth.py
    _DiscountWrite = Authenticator(
        required_scopes={Scope.web_default, Scope.discounts_write},
        allowed_subjects={User, Organization},
    )
    DiscountWrite = Annotated[AuthSubject[User | Organization], Depends(_DiscountWrite)]
  • Specific Cases: For API endpoints that should only be used in the context of our web dashboard or our internal backoffice, use one of the predefined authenticator dependencies from server/polar/auth/dependencies.py.

    • WebUser: Requires a logged-in user (AuthSubject[User]).
    • WebUserOrAnonymous: Allows either a logged-in user or an anonymous user (AuthSubject[User | Anonymous]).
    • AdminUser: Requires a user with admin privileges.
  • Scopes: Scopes are used to control access to specific operations. An AuthSubject has a set of scopes, and an Authenticator can define a set of required_scopes. Access is granted if the subject possesses at least one of the required scopes.

  • Example:

    from polar.models import User
    from polar.discount.auth import DiscountWrite
    
    @router.post("/discounts")
    def create_discount(auth_subject: DiscountWrite) -> Discount:
        # Only users/orgs with `web_default` or `discounts_write` can access this
        ...
  • How it works: The system checks for credentials in a specific order: customer session token, user session cookie, and various API tokens (OAuth2, Personal Access, Organization Access). If no valid credential is found, it defaults to an Anonymous subject. The endpoint's authenticator then validates if the resolved subject type and its scopes are allowed.

Key Integrations

  • Stripe: Handles payments and subscriptions. Requires API keys and a webhook secret in server/.env.
  • GitHub: Used for authentication and repository-related features. Requires a GitHub App to be configured for local development.

Documentation

The documentation is located in the docs/ directory and is generated by Mintlify. It serves as a reference for developers and users.

From the docs/ directory, it can be built and served locally with:

pnpm dev