Flask recipe

OpenReceive ships no openreceive.flask package — on purpose. The engine’s HTTP handler is framework-free, and a Flask Blueprint over it is about forty lines, all of them below. This recipe is the supported Flask integration; it becomes a package (openreceive[flask]) once two things are true: it has been used by someone outside this repository, and its code has not changed for a release. Not before. If you copy it and hit a rough edge, that is exactly the report that moves it.

Install the engine with the SQLAlchemy extra (Flask ≥ 3, Python ≥ 3.10):

pip install "openreceive[sqlalchemy]" flask flask-login flask-wtf

The blueprint

OpenReceiveApp is the storage-aware engine: your Host, OpenReceive’s own SQLAlchemy repository over your Engine, and the durably gated opportunistic reconcile. Flask’s job is one translation each way — flask.request into the engine’s HttpRequest, the engine’s (status, body, headers) into a Response. The engine’s cross-site refusal, JSON gate, declared-fields check and 64 KB body cap all run unchanged.

# openreceive_blueprint.py
import os
from flask import Blueprint, Response, request
from flask_login import current_user
from sqlalchemy import create_engine
from openreceive.nwc.receive_client import NwcReceiveClient
from openreceive.server import Host, HttpRequest, OpenReceiveApp, Service
from openreceive.storage.sql import SqlPaymentRepository
from .models import orders  # your order model


def openreceive_blueprint(engine, *, prefix="/openreceive", rate_limiting=False):
    host = Host(
        amount_for=lambda reference: (
            {"currency": "USD", "value": str(o.total), "description": o.summary}
            if (o := orders.find(reference)) else None
        ),
        # `context.request` is the Flask request; Flask-Login's proxy is
        # request-bound, so the same check your order page makes works here.
        authorize=lambda context: current_user.is_authenticated
        and orders.owned_by(context.resource["reference"], current_user.id),
        # Inside the settlement transaction: write through settlement.connection.
        on_paid=lambda s: s.connection.execute(orders.claim_paid(s.reference, s.paid_at)),
    )
    state = {}

    def app():  # the wallet preflight runs on first use, never at import
        if "app" not in state:
            service = Service(NwcReceiveClient(os.environ["NWC_URI"].strip()))
            state["app"] = OpenReceiveApp(
                service=service, host=host, repository=SqlPaymentRepository(engine),
                prefix="", rate_limiting=rate_limiting,
                client_ip=lambda req: req.remote_addr,
            )
        return state["app"]

    bp = Blueprint("openreceive", __name__, url_prefix=prefix)

    @bp.route("/<path:rest>", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
    def dispatch(rest):
        wire = HttpRequest(
            method=request.method, path=f"/{rest}", query_string=request.query_string.decode(),
            headers=dict(request.headers), remote_addr=request.remote_addr,
            content_length=request.content_length,
            # A reader, so the engine caps the body BEFORE reading it.
            body=lambda max_bytes: request.stream.read(max_bytes),
            framework_request=request,
        )
        status, body, headers = app().handle(wire)
        return Response(app().handler.error_response and __import__("json").dumps(body), status, headers, mimetype="application/json")

    bp.preflight = app  # call once at boot to fail closed (below)
    return bp

The last line of dispatch is shorter in practice — HttpResponse.json() is the compact serializer: response = app().handle(wire) then Response(response.json(), response.status, response.headers, mimetype="application/json").

Wiring it

# app.py
from flask import Flask
from flask_wtf.csrf import CSRFProtect
from sqlalchemy import create_engine
from .openreceive_blueprint import openreceive_blueprint

app = Flask(__name__)
csrf = CSRFProtect(app)
engine = create_engine(os.environ["DATABASE_URL"])   # OpenReceive's own Engine, same database

bp = openreceive_blueprint(engine, rate_limiting=True)
app.register_blueprint(bp)
# Flask-WTF checks a token on every POST; the checkout sends it as a header
# (below), so the blueprint keeps CSRF protection rather than exempting it.

# Fail closed at boot, the way the FastAPI lifespan and the Rails engine do:
# a dead relay or a spend-capable code stops the deploy, not the first payer.
# Flask 3 has no `before_serving`; run the preflight where your app boots
# (the module, a `create_app()` factory, or a gunicorn `on_starting` hook) and
# skip it in `flask db upgrade`-style commands that have no wallet access.
if os.environ.get("OPENRECEIVE_PREFLIGHT", "1") == "1":
    bp.preflight()

Migrate the two tables through your own tooling first — openreceive scaffold payments --alembic --dialect postgres (Flask-Migrate is Alembic) or --sql.

CSRF: the header

Flask-WTF reads the token from the X-CSRFToken header as well as a form field. Render it into the page and tell the checkout which header carries it — csrf-header on the element, csrfHeader on the React/Vue/Svelte/Angular wrappers — so every body-bearing request the checkout makes (/checkouts, /payments/check, the swap routes) passes CSRFProtect:

<meta name="csrf-token" content="{{ csrf_token() }}">
<openreceive-checkout reference="{{ order.id }}" prefix="/openreceive"
                      csrf-header="X-CSRFToken"></openreceive-checkout>

Hosts without a bundler load @openreceive/elements’s standalone build from a static directory; with one, import "@openreceive/elements" (or the React <Checkout csrfHeader="X-CSRFToken" …/>). The engine’s own Sec-Fetch-Site refusal still applies underneath.

Worker, doctor, reconcile

The openreceive CLI takes --app module:attr naming an OpenReceiveApp or a zero-argument callable returning one — bp.preflight above is exactly that:

openreceive doctor --app app:bp.preflight
openreceive reconcile --app app:bp.preflight
openreceive notifications --app app:bp.preflight     # the optional NWC-02 listener

Sec-Fetch-Site, the authorize context, swap_data, rate limiting and the storage rules are the same as for FastAPI — read quickstart-fastapi.md for the prose; only the Flask glue above is different.