| name | stk-dev |
| description | Background knowledge for stk async Quart framework development. Auto-loads when working on stk-based code: models, views, templates, database queries, auth patterns, Vue frontend. Provides conventions, patterns, and gotchas so Claude writes correct async Quart code instead of Flask patterns.
|
| user-invocable | false |
stk Framework Conventions
Async Quart + SQLAlchemy 2.x async + quart-security + Vue 3/Vuetify 3 (Options API). No build step.
Critical Rules
- ALL route handlers are
async def. ALL DB operations use await.
- DB sessions via
g.db_session (request-scoped), NOT a global db object.
- Models inherit from
Base (plain DeclarativeBase), NOT db.Model.
- Imports:
from quart import ..., NOT from flask import ....
- Auth:
from quart_security import ..., NOT from flask_security import ....
- Relationships MUST use
lazy="selectin" for async compatibility.
- Vue delimiters are
${}, NOT {{}} (conflicts with Jinja). Access via config.delimiters.
- Vue uses Options API (
data(), methods, mounted()), NOT Composition API.
- No Celery. Background tasks via
stk.tasks.run_in_background().
- Pagination is manual:
offset().limit() + select(func.count()).
- Icons: Tabler Icons (
ti ti-*), NOT Material Design Icons.
- JSON serialization for list endpoints:
orjson via import orjson as json.
- Frontend sends mutations wrapped:
{item: {...}}, extract with json_data.get("item", {}).
DB Access Patterns
from quart import g
from sqlalchemy import select, func
result = await g.db_session.execute(select(Model).where(Model.active == True))
items = result.scalars().all()
item = await g.db_session.get(Model, id)
total = await g.db_session.scalar(select(func.count()).select_from(Model))
import stk.extensions as ext
async with ext.async_session_factory() as session:
...
Auth Decorators
Two patterns exist:
@bp.before_request
@auth_required("session")
@roles_required("admin")
async def before_request():
pass
@bp.get("/things/")
async def things_page():
...
from quart_security import auth_required, roles_required, current_user
@bp.get("/protected")
@auth_required("session")
async def protected():
...
Model Patterns
Models use @dataclasses.dataclass decorator and inherit from Base:
import dataclasses
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship
from stk.extensions import Base
@dataclasses.dataclass
class Thing(Base):
__tablename__ = "things"
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.now, nullable=False)
user_id = Column(Integer, ForeignKey("user.id"))
user = relationship("User", lazy="selectin")
def to_dict(self):
return {"id": self.id, "name": self.name, "active": self.active}
async def from_dict(self, data):
self.name = data.get("name", self.name)
self.active = data.get("active", self.active)
Async API Endpoint Pattern
import logging
import orjson as json
from quart import Blueprint, Response, g, render_template, request
from quart_security import auth_required, current_user, roles_required
from sqlalchemy import func, select
from stk.user.models import Activity
from .models import Thing
log = logging.getLogger(__name__)
bp = Blueprint("things", __name__)
PER_PAGE = 25
@bp.before_request
@auth_required("session")
@roles_required("admin")
async def before_request():
pass
@bp.get("/things/")
async def things_page():
return await render_template("things/index.html")
@bp.get("/api/things")
async def list_things():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", PER_PAGE, type=)
query = select(Thing)
search := request.args.get():
query = query.where(Thing.name.ilike())
count_result = g.db_session.execute(select(func.count()).select_from(Thing))
total = count_result.scalar()
result = g.db_session.execute(
query.offset((page - ) * per_page).limit(per_page)
)
items = [t.to_dict() t result.scalars().()]
response_data = {: items, : total, : per_page}
Response(json.dumps(response_data), content_type=)
():
json_data = request.json
thing_data = json_data.get(, {})
thing = Thing()
thing.from_dict(thing_data)
g.db_session.add(thing)
:
g.db_session.flush()
Activity.register(current_user., , thing.to_dict())
g.db_session.commit()
{: }
Exception:
g.db_session.rollback()
log.exception()
{: },
():
thing = g.db_session.get(Thing, )
thing :
{: },
json_data = request.json
thing_data = json_data.get(, {})
old_data = thing.to_dict()
:
thing.from_dict(thing_data)
Activity.register(
current_user., ,
{: old_data, : thing.to_dict()},
)
g.db_session.commit()
{: }
Exception:
g.db_session.rollback()
log.exception()
{: },
():
thing = g.db_session.get(Thing, )
thing :
{: },
thing_data = thing.to_dict()
:
g.db_session.delete(thing)
Activity.register(current_user., , thing_data)
g.db_session.commit()
{: }
Exception:
g.db_session.rollback()
log.exception()
{: },
Activity Logging
Always log admin/mutation actions. Async, broadcasts to WebSocket:
await Activity.register(current_user.id, "Action Name", {"key": "value"})
Vue Frontend Pattern (Options API)
Templates use Options API with layoutMixin, config.delimiters, and registerStkComponents:
{% extends "layout.html" %}
{% block content %}
<v-card class="ma-2 mt-12 w-100 h-100">
<v-toolbar>
<v-toolbar-title>Things</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-card-text>
<v-data-table-server
:items="items" :items-length="itemsLength"
:headers="headers"
:page="options.page" :items-per-page="options.itemsPerPage"
@update:options="refresh" hover
>
<template v-slot:top>
<v-toolbar dense elevation="0" color="transparent">
<v-btn class="ml-auto" @click="createItem" size="small" color="primary">
<template v-slot:prepend><i =>
Add Thing
ti ti-pencil
ti ti-trash
Thing Editor
Save
${snackMessage}
{% endblock %}
{% block js %}
{% endblock %}
Passing Server Data to Vue
Use <script type="application/json"> tags parsed in data():
<script type="application/json" id="roles-data">
{{ roles|tojson|safe }}
</script>
data() {
return {
roles: JSON.parse(document.querySelector('#roles-data').textContent),
};
}
Navigation Sidebar
Add entries in stk/static/js/navigation.js. Items support role-based visibility via role (singular string). Supports nested children for grouped items:
{ title: 'Dashboard', icon: 'ti ti-home', to: '/dashboard' },
{ heading: 'Administration' },
{ title: 'Activity Logs', icon: 'ti ti-history', to: '/activities', role: 'admin' },
{
title: 'User Management', icon: 'ti ti-users-group', role: 'admin',
children: [
{ title: 'Users', icon: 'ti ti-users', to: '/users' },
{ title: 'Roles', icon: 'ti ti-shield', to: '/roles' },
]
},
Template Block Structure
{% extends "layout.html" %}
{% block css %}{% endblock %}
{% block content %}{% endblock %}
{% block js %}{% endblock %}
Background Tasks
from stk.tasks import run_in_background, run_with_session
await run_in_background(send_notification(user_id))
async def heavy_work(session):
item = await session.get(Model, item_id)
item.status = "processed"
await run_with_session(heavy_work)
For detailed examples see references/patterns.md.