---
name: datasette-plugins
description: "Writing Datasette plugins using Python and the pluggy plugin system. Use when Claude needs to: (1) Create a new Datasette plugin, (2) Implement plugin hooks like prepare_connection, register_routes, render_cell, etc., (3) Add custom SQL functions, (4) Create custom output renderers, (5) Add authentication or permissions logic, (6) Extend Datasette's UI with menus, actions, or templates, (7) Package a plugin for distribution on PyPI"
---
# Datasette Plugin Development
## Overview
Datasette plugins extend Datasette's functionality using Python and the [pluggy](https://pluggy.readthedocs.io/) plugin system. Plugins can add SQL functions, custom routes, authentication, UI elements, and more.
## Quick Start: One-off Plugin
Create `plugins/my_plugin.py`:
```python
from datasette import hookimpl
@hookimpl
def prepare_connection(conn):
conn.create_function("hello_world", 0, lambda: "Hello world!")
```
Run with: `datasette serve mydb.db --plugins-dir=plugins/`
## Installable Plugin Structure
For distributable plugins, use this structure:
```
datasette-my-plugin/
├── pyproject.toml
├── datasette_my_plugin/
│ ├── __init__.py # Plugin implementation
│ ├── static/ # Optional: JS/CSS files
│ └── templates/ # Optional: Jinja2 templates
└── tests/
└── test_plugin.py
```
### pyproject.toml
```toml
[project]
name = "datasette-my-plugin"
version = "0.1.0"
description = "My Datasette plugin"
requires-python = ">=3.10"
dependencies = ["datasette"]
[dependency-groups]
dev = [
"pytest",
"pytest-asyncio"
]
[project.entry-points.datasette]
my_plugin = "datasette_my_plugin"
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
```
## Core Plugin Hooks
See [references/hooks.md](references/hooks.md) for complete hook documentation.
### Most Common Hooks
| Hook | Purpose |
|------|---------|
| `prepare_connection(conn, database, datasette)` | Register custom SQL functions |
| `register_routes(datasette)` | Add custom URL routes |
| `startup(datasette)` | Initialize on server start |
| `render_cell(row, value, column, table, database, datasette, request)` | Customize cell display |
| `extra_template_vars(...)` | Add template variables |
| `actor_from_request(datasette, request)` | Custom authentication |
| `permission_allowed(datasette, actor, action, resource)` | Custom permissions |
### Example: Custom SQL Function
```python
from datasette import hookimpl
import hashlib
@hookimpl
def prepare_connection(conn):
conn.create_function("md5", 1, lambda s: hashlib.md5(s.encode()).hexdigest())
```
### Example: Custom Route
```python
from datasette import hookimpl, Response
@hookimpl
def register_routes():
return [
(r"^/-/my-page$", my_page_view),
]
async def my_page_view(datasette, request):
return Response.html("
My Custom Page
")
```
### Example: Startup Hook
```python
@hookimpl
def startup(datasette):
async def inner():
db = datasette.get_database()
await db.execute_write("""
CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, data TEXT)
""")
return inner
```
## Plugin Configuration
Plugins read configuration from `datasette.yaml`:
```yaml
plugins:
datasette-my-plugin:
option1: value1
option2: value2
```
Access in plugin:
```python
@hookimpl
def startup(datasette):
config = datasette.plugin_config("datasette-my-plugin") or {}
my_option = config.get("option1", "default")
```
### Secret Configuration
Use environment variables:
```yaml
plugins:
datasette-my-plugin:
api_key:
$env: MY_API_KEY
```
Or files:
```yaml
plugins:
datasette-my-plugin:
api_key:
$file: /secrets/api-key
```
## Testing Plugins
```python
from datasette.app import Datasette
import pytest
@pytest.mark.asyncio
async def test_plugin_installed():
datasette = Datasette(memory=True)
response = await datasette.client.get("/-/plugins.json")
assert response.status_code == 200
plugins = {p["name"] for p in response.json()}
assert "datasette-my-plugin" in plugins
@pytest.mark.asyncio
async def test_custom_route():
datasette = Datasette(memory=True)
response = await datasette.client.get("/-/my-page")
assert response.status_code == 200
assert "My Custom Page" in response.text
```
Run tests: `pytest`
## Response Types
```python
from datasette import Response
# HTML response
Response.html("Hello
")
# JSON response
Response.json({"key": "value"})
# Text response
Response.text("Plain text")
# Redirect
Response.redirect("/other-page")
# Custom response
Response(body, content_type="text/plain", status=200, headers={})
```
## URL Design
Use `/-/` prefix to avoid conflicts with database names:
- `/-/my-feature` - Global feature
- `/dbname/-/my-feature` - Database-specific
- `/dbname/tablename/-/my-feature` - Table-specific
## Static Assets & Templates
Static files in `static/` are served at:
`/-/static-plugins/PLUGIN_NAME/filename.js`
Templates in `templates/` override Datasette defaults. Priority:
1. `--template-dir` argument
2. Plugin templates
3. Datasette defaults
## Common Patterns
### Adding Menu Items
```python
@hookimpl
def menu_links(datasette, actor):
return [{"href": "/-/my-page", "label": "My Feature"}]
```
### Table Actions
```python
@hookimpl
def table_actions(datasette, actor, database, table):
return [{"href": f"/{database}/{table}/-/action", "label": "My Action"}]
```
### Custom Output Renderer
```python
@hookimpl
def register_output_renderer(datasette):
return {
"extension": "csv",
"render": render_csv,
}
async def render_csv(datasette, columns, rows):
# Return Response object
pass
```
### Event Tracking
```python
@hookimpl
def track_event(datasette, event):
print(f"Event: {event.name}, Actor: {event.actor}")
```
## Debugging
Enable hook tracing:
```bash
DATASETTE_TRACE_PLUGINS=1 datasette mydb.db
```
## Key Imports
```python
from datasette import hookimpl, Response
from datasette.app import Datasette
from datasette.filters import FilterArguments
from datasette.permissions import Action, Resource, PermissionSQL
import markupsafe # For safe HTML in render_cell
```