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-RESTful 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-RESTful 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_restful.Resource 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