Use your own engine and Alembic migrations
If your server already has a database and migrations, the instrumentation can live in the same database under its own table prefix, with its tables managed by the same Alembic history.
Pass your engine
Section titled “Pass your engine”DatabaseSink accepts an AsyncEngine instead of a URL. It never creates tables
unless asked and never disposes an engine it did not create.
import os
from fastmcp import FastMCPfrom fastmcp_feedback.instrumentation import DatabaseSink, instrumentfrom sqlalchemy.ext.asyncio import create_async_engine
app = FastMCP("My Server")engine = create_async_engine(os.environ["DATABASE_URL"]) # the engine your app already has
sink = DatabaseSink(engine, prefix="ffb_")mw = instrument(app, [sink])prefix sets the table names: {prefix}tool_calls, {prefix}events,
{prefix}feedback_call_links and {prefix}embeddings. The default is ffb_.
Add the tables to Alembic
Section titled “Add the tables to Alembic”build_metadata(prefix) returns a SQLAlchemy MetaData holding the four
tables. Give it to Alembic next to your own metadata in env.py:
from fastmcp_feedback.instrumentation import build_metadatafrom myapp.models import Base # your declarative base
target_metadata = [Base.metadata, build_metadata(prefix="ffb_")]Then generate a migration as usual:
alembic revision --autogenerate -m "fastmcp-feedback tables"alembic upgrade headThe prefix must match the one you pass to DatabaseSink.
Without embeddings
Section titled “Without embeddings”build_metadata includes ffb_embeddings by default, sized for 1024-dimension
vectors, and on PostgreSQL its embedding column needs the pgvector extension.
If you do not embed, leave the table out:
target_metadata = [Base.metadata, build_metadata(prefix="ffb_", embedding_dim=None)]If you do embed, pass your embedder’s dimension (embedding_dim=768, say) and
use the same value for DatabaseSink(embedding_dim=...).
Upgrading from older releases
Section titled “Upgrading from older releases”create_tables=True creates missing tables but never alters existing ones.
Columns added since the first release need a migration:
| Added in | Change |
|---|---|
| 2026.09.27.3 | table ffb_feedback_call_links |
| 2026.09.27.4 | ffb_tool_calls.outcome VARCHAR(16), indexed |
| 2026.09.28 | ffb_tool_calls.sample_rate FLOAT |
| 2026.09.29 | table ffb_events |
| 2026.09.30 | table ffb_embeddings |
Until a column or table exists, writes that need it fail and those records are
dropped and logged. Calls and events are written in separate transactions, so
a database without ffb_events still stores calls.