Framework guide

Python

Install the Python SDK and build auth and data flows in FastAPI or Flask — with pagination, optimistic concurrency, atomic batches and typed errors.

Python 3.9+FastAPI & FlaskTyped, py.typed

Python means server, which means secret key

Almost all Python runs on a server, so secret_key is usually right. It carries full authority over every user in the app — which means the per-user scoping a browser gets automatically is now your job. Reach for publishable_key only when your service acts on behalf of one signed-in end user.

  1. Install the SDK

    Bash
    pip install neuctra-authix
    
    # or with poetry / uv
    poetry add neuctra-authix
    uv add neuctra-authix
  2. Add your keys

    Bash
    # .env — never commit this file
    AUTHIX_APP_ID=app_xxxxxxxxxxxx
    AUTHIX_SECRET_KEY=sk_live_xxxxxxxx_xxxxxxxxxxxxxxxx
  3. Create the client

    Python
    1# app/authix.py
    2import os
    3
    4from neuctra_authix import Authix
    5
    6# One client for the process. It holds a connection pool, so building a new one
    7# per request throws away every kept-alive connection.
    8authix = Authix(
    9    app_id=os.environ["AUTHIX_APP_ID"],
    10    secret_key=os.environ["AUTHIX_SECRET_KEY"],
    11)

    Keys are checked before any request

    Passing a pk_ key as secret_key, or a malformed key, raises ConfigurationError at construction — so a misconfigured deploy fails at boot rather than on its first real request.

  4. Make your first calls

    Python
    1from neuctra_authix import Authix, VersionConflictError
    2
    3authix = Authix(app_id=APP_ID, secret_key=SECRET_KEY)
    4
    5# Create a user
    6result = authix.signup_user(
    7    name="Ada Lovelace",
    8    email="[email protected]",
    9    password="a-strong-password",
    10)
    11user_id = result["user"]["id"]
    12
    13# Store a record they own
    14record = authix.add_user_data(
    15    user_id=user_id,
    16    data_category="notes",
    17    data={"title": "First note", "body": "Hello world"},
    18)
    19
    20print(record["data"]["id"], record["data"]["version"])

Reading data

Every list and search is bounded. There is no “fetch everything” mode, because one such call against a large app would load the whole dataset into memory.

Python
1# One page — 20 records by default, 100 maximum.
2page = authix.get_user_data(user_id=user_id, category="notes", limit=50)
3
4for note in page:          # a Page iterates its records
5    print(note["title"])
6
7page.has_more              # bool
8page.next_cursor           # pass back as cursor=... for the next page
9
10# Or let the SDK follow the cursor for you.
11for note in authix.iter_user_data(user_id=user_id, category="notes"):
12    process(note)
13
14# Bound the traversal when the size is unknown.
15for row in authix.iter_all_users_data(q="invoice", max_pages=10):
16    process(row)

FastAPI

Auth endpoints, plus the scoping pattern that matters most.

Python
1# app/main.py
2from fastapi import Depends, FastAPI, HTTPException, Request
3from pydantic import BaseModel
4
5from neuctra_authix import (
6    AuthenticationError,
7    AuthixAPIError,
8    InsufficientScopeError,
9    NotFoundError,
10    ValidationError,
11)
12
13from .authix import authix
14
15app = FastAPI()
16
17
18class Credentials(BaseModel):
19    email: str
20    password: str
21
22
23class SignupBody(Credentials):
24    name: str
25
26
27@app.post("/auth/signup", status_code=201)
28def signup(body: SignupBody):
29    result = authix.signup_user(
30        name=body.name, email=body.email, password=body.password
31    )
32
33    # An unverified account can sign in but cannot store records, so send the
34    # code now rather than letting the first write fail with a 403.
35    authix.request_email_verification_otp(
36        user_id=result["user"]["id"], email=body.email
37    )
38
39    return {"user_id": result["user"]["id"]}
40
41
42@app.post("/auth/login")
43def login(body: Credentials):
44    return authix.login_user(email=body.email, password=body.password)
45
46
47def current_user(request: Request) -> str:
48    """Your own session check. Whatever it is, the id must come from here."""
49    user_id = request.session.get("user_id")
50    if not user_id:
51        raise HTTPException(status_code=401, detail="Not signed in")
52    return user_id
53
54
55@app.get("/notes")
56def list_notes(cursor: str | None = None, user_id: str = Depends(current_user)):
57    page = authix.get_user_data(
58        user_id=user_id,          # <- trusted, never from the request body
59        category="notes",
60        limit=20,
61        cursor=cursor,
62    )
63
64    return {
65        "data": page.data,
66        "has_more": page.has_more,
67        "next_cursor": page.next_cursor,
68    }

Never take the user id from the request

With a secret key, user_id is whatever you pass. If it comes from the request body or a query parameter, any caller can read any user by changing one value — and your own client will never reveal the bug, because it always sends the right id.

Turning SDK errors into HTTP responses

Every failure is a typed exception, so you can map them once and forget about status codes at the call site.

Python
1# app/errors.py
2from fastapi import Request
3from fastapi.responses import JSONResponse
4
5from neuctra_authix import (
6    AuthixAPIError,
7    InsufficientScopeError,
8    VersionConflictError,
9)
10
11
12def install(app):
13    @app.exception_handler(AuthixAPIError)
14    def handle(request: Request, exc: AuthixAPIError):
15        # A scope error means this server used the wrong key. That is our bug,
16        # so it must not surface as a 403 the caller might try to "fix".
17        if isinstance(exc, InsufficientScopeError):
18            app.logger.error("wrong Neuctra Authix key for %s: %s", request.url.path, exc.hint)
19            return JSONResponse({"message": "Server misconfiguration."}, 500)
20
21        body = {"message": exc.message, "code": exc.code}
22
23        if isinstance(exc, VersionConflictError):
24            body["current_version"] = exc.current_version
25
26        # Passing the status through keeps a 409 a 409 — the caller can retry
27        # a conflict, and can do nothing at all with a 500.
28        return JSONResponse(body, exc.status)

Flask

The same shape, with Flask's idioms.

Python
1# app.py  (Flask)
2from flask import Flask, g, jsonify, request, session
3
4from neuctra_authix import AuthixAPIError
5
6from .authix import authix
7
8app = Flask(__name__)
9
10
11@app.before_request
12def load_user():
13    g.user_id = session.get("user_id")
14
15
16@app.errorhandler(AuthixAPIError)
17def on_authix_error(exc):
18    return jsonify(message=exc.message, code=exc.code), exc.status
19
20
21@app.get("/notes")
22def list_notes():
23    if not g.user_id:
24        return jsonify(message="Not signed in"), 401
25
26    page = authix.get_user_data(
27        user_id=g.user_id,
28        category="notes",
29        limit=20,
30        cursor=request.args.get("cursor"),
31    )
32
33    return jsonify(
34        data=page.data, has_more=page.has_more, next_cursor=page.next_cursor
35    )

Two writers, one record

Pass the version you last read and a competing change is rejected instead of silently overwritten.

Python
1from neuctra_authix import VersionConflictError
2
3record = authix.get_single_user_data(user_id=user_id, data_id=data_id)
4
5try:
6    authix.update_user_data(
7        user_id=user_id,
8        data_id=data_id,
9        data={"status": "shipped"},
10        version=record["data"]["version"],
11    )
12except VersionConflictError as exc:
13    # Another writer got there first. Nothing was overwritten.
14    print("changed since read; now at version", exc.current_version)

Changing records together

When a partial write would leave your data inconsistent.

Python
1# Every operation commits, or none of them do.
2authix.batch(
3    user_id=user_id,
4    operations=[
5        {"type": "create", "dataCategory": "orders", "total": 40},
6        {"type": "update", "id": stock_id, "version": 3, "remaining": 9},
7        {"type": "delete", "id": draft_id},
8    ],
9)

Searching

keys is an exact match, q is a substring match. Both execute in the database against an index, not in Python.

Python
1# Exact field match — a containment query, index-backed in Postgres.
2paid = authix.search_in_user_data(user_id=user_id, keys={"status": "paid"})
3
4# Substring match across the whole record.
5invoices = authix.search_in_all_app_users_data(q="invoice", limit=100)
6
7# Across users — secret key only.
8admins = authix.search_in_all_app_users(keys={"role": "admin", "isActive": True})

User search is restricted to an allowlist — id, username, name, email, phone, address, role, isVerified, isActive. Filtering on anything else raises ValidationError, so credential columns are never reachable.

Short-lived processes

Scripts and jobs should release the connection pool when they finish.

Python
1# Short-lived processes — a CLI, a cron job, a test — should close the
2# connection pool deterministically.
3with Authix(app_id=APP_ID, secret_key=SECRET_KEY) as authix:
4    for user in authix.iter_all_users():
5        print(user["email"])

Project structure

A layout that keeps the scoping decision somewhere you cannot miss it.

Bash
app/
├── authix.py          # one client for the process
├── main.py            # FastAPI app + routes
├── errors.py          # AuthixAPIError -> HTTP responses
└── routes/
    ├── auth.py        # signup, verify, login
    ├── notes.py       # per-user data, scoped by YOU
    └── admin.py       # cross-user reads

Common mistakes

  • Building a client per request. It discards the connection pool. Build one at import time and reuse it.
  • Trusting a client-supplied user id. The single most common way a server-side integration leaks data.
  • Catching bare Exception. Catch VersionConflictError or NotFoundError and act on them; letting the rest surface tells you when something is genuinely wrong.
  • Expecting an unbounded read. Use iter_* with max_pages.
  • Skipping email verification. The account can log in, but every write fails with 403 until it is verified.

You now have

  • Signup, verification and login endpoints.
  • Per-user reads scoped to a session you verified.
  • Paged traversal that will not exhaust memory.
  • Errors mapped once, with their status codes intact.

Retrying a conflict

A VersionConflictError is not a failure to log and forget — it means someone else wrote first. Re-read the record, reapply your change to the fresh version, and try again. Two or three attempts is plenty; beyond that something else is wrong.

Related