Skip to content

Capture the current user for audit fields

Every revision records created_by / updated_by. By default they are "anonymous", because SpecStar doesn't know who the caller is until you wire it up. This page shows how to populate them with the real user — and one pitfall that silently keeps them "anonymous".

The cleanest approach is a FastAPI dependency that returns the current user. Pass it to the DependencyProvider; SpecStar uses its return value for every write's audit fields. It can use any FastAPI features (Header, Depends, …):

from fastapi import Header
from specstar import spec
from specstar.crud.route_templates.basic import DependencyProvider


async def get_current_user(authorization: str | None = Header(default=None)) -> str:
    return resolve_user(authorization) or "anonymous"


spec.configure(dependency_provider=DependencyProvider(get_user=get_current_user))

A real get_user always wins over default_user / per-model defaults, so authenticated requests record the actual user.

Alternative: a ContextVar (when the user is set elsewhere, e.g. middleware)

If the user is established outside the route (middleware, a shared dependency), you can read it from a ContextVar:

import contextvars
from fastapi import Depends, FastAPI, Header
from specstar import spec

current_user: contextvars.ContextVar[str] = contextvars.ContextVar(
    "current_user", default="anonymous"
)


# IMPORTANT: this dependency MUST be `async def`. See the pitfall below.
async def bind_current_user(authorization: str | None = Header(default=None)):
    current_user.set(resolve_user(authorization) or "anonymous")


app = FastAPI(dependencies=[Depends(bind_current_user)])
spec.configure(default_user=lambda: current_user.get())
spec.apply(app)

⚠️ Pitfall: a sync dependency silently records anonymous

FastAPI runs a plain def (sync) dependency in a threadpool. A ContextVar set in that worker thread does not propagate back to the request handler, so current_user.get() returns the default and created_by is silently "anonymous". Make the dependency async def (it runs in the request's own context) and the value propagates. If you can't, prefer the get_user dependency above, which returns the value directly and has no such caveat.

Programmatic writes

For non-HTTP code, set the user via configuration or per call:

spec.configure(default_user="batch-job")            # process-wide default
# or, per operation:
with mgr.using(user="batch-job", now=datetime.now(timezone.utc)):
    mgr.create(...)

Without any of these, non-strict writes fall back to "anonymous" + now() (matching an unauthenticated HTTP request); set strict_operation_context=True to make a missing context a hard error instead.