Phoenix Backend Development
Phoenix is an AI observability platform. The backend is Python: FastAPI serving a REST API and Strawberry
GraphQL API over an async SQLAlchemy ORM (PostgreSQL + SQLite).
Development Guide Index
Read DEVELOPMENT.md (env setup, uv, tests, debugpy, pre-commit, REST API conventions) and CONTRIBUTING.md (PR format, conventional commits, code review expectations) if you have not already.
Everyday Commands
make dev-backend # backend only, no frontend build needed
uv run pytest path/to/test -n auto # run specific tests in parallel
make test-python # full test suite
make graphql # regenerate schema after GQL changes
make format # format all code
make typecheck-python # mypy + pyright
Key Directories
src/phoenix/server/api/
mutations/ Domain-specific mutation mixins, composed in __init__.py
types/ GraphQL types with field resolvers
input_types/ Strawberry @input classes with validation
subscriptions.py Async generator subscriptions (streaming)
queries.py Root query type
context.py Request context: db, dataloaders, auth, event queue
dataloaders/ Batch loaders (prevent N+1 queries)
auth.py Permission classes (IsNotReadOnly, IsNotViewer, etc.)
routers/ REST API endpoints (v1/)
src/phoenix/db/
models.py SQLAlchemy ORM models (single file)
migrations/ Alembic migrations
tests/unit/server/api/
mutations/ Mutation tests
types/ Type resolver tests
conftest.py Fixtures: db, gql_client, test data factories
What Are You Doing?
| Task |
Reference |
| Adding or modifying a mutation, type, subscription, or input |
references/graphql-patterns.md |
| Writing or modifying tests |
references/test-patterns.md |
| Writing tests for code that emits OpenInference spans (VCR cassettes, span attribute assertions) |
references/llm-trace-tests.md |
| Adding a migration or modifying database models |
references/database-patterns.md |
Hard Rules
- Side effects belong on
Mutation, not Query. A resolver that makes outbound
network calls, reads secrets, writes state, or accepts a user-supplied URL/host
MUST be a @strawberry.mutation with permission_classes=[...]. Query fields
bypass the make check-graphql-permissions CI guard and are reachable
unauthenticated by default — this has been exploited as an SSRF vector. See
references/graphql-patterns.md → "Query vs Mutation".
Tests
- Never sleep to wait for a daemon. Unit-test apps run the server's daemons in the
test's event loop, so a fixed sleep ties the outcome to machine load. Patch the daemon's
sleep so it parks on an event; the test releases it once and awaits its return to the
parked state, with a timeout that fails the test by name. See
references/test-patterns.md → "Waiting on Daemons".
- Any
import phoenix.<anything> imports the whole server. The package init pulls in the
session module and with it the app, several seconds per process, including pytest plugins
and scripts. Keep new imports out of src/phoenix/__init__.py.
- Unit-test apps take startup shortcuts, most with an opt-out marker. The conftest
memoizes key derivation, the GraphQL schema, routers, and FastAPI's route analysis across
apps in a worker, stubs out the docs MCP session, model-cost seeding, and other startup
effects no test observes, and seeds each worker's template database with the startup rows.
A test whose subject is one of those behaviors must opt out with its marker or it passes
against the shortcut. The markers registered in the unit conftest are the authoritative
list. See
references/test-patterns.md → "Startup Shortcuts".
Naming
- Avoid acronyms and single/double-letter abbreviations for local variables.
Prefer the full noun:
session / project_session over ps, trace over t,
example / dataset_example over de. The cost of a longer identifier is trivial; the
cost of having to mentally expand an acronym while reading unfamiliar code is
not.
- Established domain acronyms used in the codebase (
db, gql, otel, llm)
are fine — they're vocabulary, not abbreviations of local nouns.
Docstrings
The project rule of "default to no comments" is about inline comments, not
docstrings. Public APIs should be documented.
- Document parameters and return values on public methods of reusable classes
(clients, services, factories, builders). Use Google-style
Args: / Returns:
/ Raises: blocks when the meaning isn't fully recoverable from the type
signature. Do not strip these during refactors — semantics outlive file moves.
- Describe behavior, not implementation. A method on a docs-search client
says "Invoke a backend tool and return its text result", not "Invoke a tool
on the MCP server" — the underlying transport is an implementation detail and
the docstring should survive a transport swap. Internal helpers (leading
_)
may reference the transport directly since their scope is bounded.
- One-liner docstrings are fine when the name and types fully convey intent
(
close(), is_backend_tool(name)). Don't pad them with restated signatures.
- Module docstrings belong at the top of any file that exposes public
surface (a client class, a router, a service module). One sentence on what
the module is for is enough.
1---2name: phoenix-server3description: Backend development guide for the Phoenix AI observability platform (Strawberry GraphQL, SQLAlchemy async, FastAPI). Use this skill when writing or modifying Python server code in the phoenix repo — adding mutations, types, migrations, or tests. Trigger on any backend task touching src/phoenix/server/, src/phoenix/db/, or tests/unit/server/.4---5
6# Phoenix Backend Development
7
8Phoenix is an AI observability platform. The backend is Python: FastAPI serving a REST API and Strawberry
9GraphQL API over an async SQLAlchemy ORM (PostgreSQL + SQLite).
10
11## Development Guide Index
12
13Read `DEVELOPMENT.md` (env setup, `uv`, tests, debugpy, pre-commit, REST API conventions) and `CONTRIBUTING.md` (PR format, conventional commits, code review expectations) if you have not already.
14
15### Everyday Commands
16
17```bash
18make dev-backend # backend only, no frontend build needed
19uv run pytest path/to/test -n auto # run specific tests in parallel
20make test-python # full test suite
21make graphql # regenerate schema after GQL changes
22make format # format all code
23make typecheck-python # mypy + pyright
24```
25
26## Key Directories
27
28```
29src/phoenix/server/api/
30 mutations/ Domain-specific mutation mixins, composed in __init__.py
31 types/ GraphQL types with field resolvers
32 input_types/ Strawberry @input classes with validation
33 subscriptions.py Async generator subscriptions (streaming)
34 queries.py Root query type
35 context.py Request context: db, dataloaders, auth, event queue
36 dataloaders/ Batch loaders (prevent N+1 queries)
37 auth.py Permission classes (IsNotReadOnly, IsNotViewer, etc.)
38 routers/ REST API endpoints (v1/)
39src/phoenix/db/
40 models.py SQLAlchemy ORM models (single file)
41 migrations/ Alembic migrations
42tests/unit/server/api/
43 mutations/ Mutation tests
44 types/ Type resolver tests
45 conftest.py Fixtures: db, gql_client, test data factories
46```
47
48## What Are You Doing?
49
50| Task | Reference |
51|------|-----------|
52| Adding or modifying a mutation, type, subscription, or input | `references/graphql-patterns.md` |
53| Writing or modifying tests | `references/test-patterns.md` |
54| Writing tests for code that emits OpenInference spans (VCR cassettes, span attribute assertions) | `references/llm-trace-tests.md` |
55| Adding a migration or modifying database models | `references/database-patterns.md` |
56
57## Hard Rules
58
59- **Side effects belong on `Mutation`, not `Query`.** A resolver that makes outbound
60 network calls, reads secrets, writes state, or accepts a user-supplied URL/host
61 MUST be a `@strawberry.mutation` with `permission_classes=[...]`. Query fields
62 bypass the `make check-graphql-permissions` CI guard and are reachable
63 unauthenticated by default — this has been exploited as an SSRF vector. See
64 `references/graphql-patterns.md` → "Query vs Mutation".
65
66## Tests
67
68- **Never sleep to wait for a daemon.** Unit-test apps run the server's daemons in the
69 test's event loop, so a fixed sleep ties the outcome to machine load. Patch the daemon's
70 sleep so it parks on an event; the test releases it once and awaits its return to the
71 parked state, with a timeout that fails the test by name. See
72 `references/test-patterns.md` → "Waiting on Daemons".
73- **Any `import phoenix.<anything>` imports the whole server.** The package init pulls in the
74 session module and with it the app, several seconds per process, including pytest plugins
75 and scripts. Keep new imports out of `src/phoenix/__init__.py`.
76- **Unit-test apps take startup shortcuts, most with an opt-out marker.** The conftest
77 memoizes key derivation, the GraphQL schema, routers, and FastAPI's route analysis across
78 apps in a worker, stubs out the docs MCP session, model-cost seeding, and other startup
79 effects no test observes, and seeds each worker's template database with the startup rows.
80 A test whose subject is one of those behaviors must opt out with its marker or it passes
81 against the shortcut. The markers registered in the unit conftest are the authoritative
82 list. See `references/test-patterns.md` → "Startup Shortcuts".
83
84## Naming
85
86- **Avoid acronyms and single/double-letter abbreviations for local variables.**
87 Prefer the full noun: `session` / `project_session` over `ps`, `trace` over `t`,
88 `example` / `dataset_example` over `de`. The cost of a longer identifier is trivial; the
89 cost of having to mentally expand an acronym while reading unfamiliar code is
90 not.
91- Established domain acronyms used in the codebase (`db`, `gql`, `otel`, `llm`)
92 are fine — they're vocabulary, not abbreviations of local nouns.
93
94## Docstrings
95
96The project rule of "default to no comments" is about **inline comments**, not
97docstrings. Public APIs should be documented.
98
99- **Document parameters and return values on public methods of reusable classes**
100 (clients, services, factories, builders). Use Google-style `Args:` / `Returns:`
101 / `Raises:` blocks when the meaning isn't fully recoverable from the type
102 signature. Do not strip these during refactors — semantics outlive file moves.
103- **Describe behavior, not implementation.** A method on a docs-search client
104 says "Invoke a backend tool and return its text result", not "Invoke a tool
105 on the MCP server" — the underlying transport is an implementation detail and
106 the docstring should survive a transport swap. Internal helpers (leading `_`)
107 may reference the transport directly since their scope is bounded.
108- **One-liner docstrings are fine** when the name and types fully convey intent
109 (`close()`, `is_backend_tool(name)`). Don't pad them with restated signatures.
110- **Module docstrings** belong at the top of any file that exposes public
111 surface (a client class, a router, a service module). One sentence on what
112 the module is for is enough.