Skip to content

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.

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 FastMCP
from fastmcp_feedback.instrumentation import DatabaseSink, instrument
from 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_.

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_metadata
from myapp.models import Base # your declarative base
target_metadata = [Base.metadata, build_metadata(prefix="ffb_")]

Then generate a migration as usual:

Terminal window
alembic revision --autogenerate -m "fastmcp-feedback tables"
alembic upgrade head

The prefix must match the one you pass to DatabaseSink.

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=...).

create_tables=True creates missing tables but never alters existing ones. Columns added since the first release need a migration:

Added inChange
2026.09.27.3table ffb_feedback_call_links
2026.09.27.4ffb_tool_calls.outcome VARCHAR(16), indexed
2026.09.28ffb_tool_calls.sample_rate FLOAT
2026.09.29table ffb_events
2026.09.30table 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.