A reference plugin for developers who want to build their own Zou/Kitsu plugins. It implements a simple ticket management system (issues on tasks) and demonstrates all the key parts of the plugin architecture:
- manifest.toml - Plugin metadata, version, and frontend flags.
- models.py - SQLAlchemy models using
db.Model,BaseMixin, andSerializerMixin. - services.py - Business logic separated from resources.
- resources.py - Flask MethodView endpoints with JWT authentication and Pydantic validation.
- init.py - Route registration and lifecycle hooks.
- migrations/ - Alembic database migrations scoped to the plugin's tables.
- frontend/ - Nuxt 4 app embedded in Kitsu via iframe.
Use this plugin as a starting point: copy the structure, rename tickets to
your plugin ID, and adapt the models and resources to your needs.
Reference: Kitsu Plugin Development
Reference: Kitsu Plugin Installation
git clone https://github.com/cgwire/kitsu-tickets.git
zou install-plugin --path ./kitsu-ticketszou install-plugin --path https://github.com/cgwire/kitsu-tickets.gitThen restart the Zou server.
tickets/
manifest.toml # Plugin ID, name, version, frontend flags
__init__.py # Routes and lifecycle hooks (pre/post install/uninstall)
models.py # SQLAlchemy models (db.Model, BaseMixin, SerializerMixin)
services.py # Business logic
resources.py # Flask MethodView endpoints (GET, POST, PUT, DELETE)
migrations/
alembic.ini # Alembic config
env.py # Migration env scoped to plugin tables
versions/ # Auto-generated migration scripts
frontend/
app/
app.vue # Nuxt root component
app.config.ts # Nuxt UI theme config
pages/
debug.vue # Example page using useKitsu() composable
logo.png # Plugin icon shown in Kitsu
| Table | Description |
|---|---|
plugin_tickets_tickets |
Tickets (title, text, status, task_id, project_id, episode_id, person_id, assignee_id) |
All routes are prefixed by /api/plugins/tickets. JWT authentication is
required.
| Method | Path | Description |
|---|---|---|
| GET | /tickets |
List all tickets |
| POST | /tickets |
Create a ticket |
| GET | /tickets/<ticket_id> |
Get a ticket |
| PUT | /tickets/<ticket_id> |
Update a ticket |
| DELETE | /tickets/<ticket_id> |
Delete a ticket |
manifest.toml declares your plugin's identity. Set frontend_project_enabled
and frontend_studio_enabled to true if your plugin ships a frontend. The
icon field accepts a Lucide icon name.
Use db.Model with BaseMixin and SerializerMixin. Prefix table names with
plugin_<id>_ to avoid collisions. BaseMixin provides helper methods:
| Method | Description |
|---|---|
Model.get(id) |
Get by primary key |
Model.get_by(**kwargs) |
Get first match by column values |
Model.get_all() |
Get all rows |
Model.create(**kwargs) |
Create and commit |
instance.update(dict) |
Update fields and commit |
instance.delete() |
Delete and commit |
Define a present() method on your models for API serialization.
Keep business logic in services.py, separate from resources. This makes
the code easier to test and reuse.
Extend flask.views.MethodView and ArgsMixin. Protect endpoints with
@jwt_required(). Use self.check_id_parameter(uuid) to validate UUID
parameters.
__init__.py exposes pre_install, post_install, pre_uninstall, and
post_uninstall hooks that run during zou install-plugin /
zou uninstall-plugin.
Run zou migrate-plugin --path . to auto-generate and apply Alembic
migrations. The migration env in migrations/env.py automatically discovers
your models.
The frontend is a standalone Nuxt 4 app. Kitsu embeds it in an iframe and
passes context via query parameters (production_id, episode_id,
dark_theme). Use the useKitsu() composable to access the Kitsu API client.
Tests use Zou's ApiDBTestCase base class. Always run tests against a test
database:
DB_DATABASE=zoudb-test python -m pytest tests/ -v