Building Extensions
Create LNbits extensions with their own API endpoints, database tables, models, migrations, and frontend templates.
Create your own LNbits extension to add custom functionality.
Extension structure
my_extension/
├── __init__.py # Entry point - defines routers and metadata
├── config.json # Extension metadata
├── views.py # UI routes (HTML pages)
├── views_api.py # API endpoints
├── crud.py # Database operations
├── models.py # Pydantic models
├── migrations.py # Database migrations
├── templates/
│ └── my_extension/
│ └── index.html # Jinja2 template
└── static/
└── js/
└── index.js # Frontend JavaScriptconfig.json
json
{
"name": "My Extension",
"short_description": "A brief description",
"tile": "/my_extension/static/image/tile.png",
"contributors": ["your-name"],
"min_lnbits_version": "1.0.0"
}Entry point (init.py)
python
from fastapi import APIRouter
from lnbits.db import Database
db = Database("ext_my_extension")
my_extension_api = APIRouter()
my_extension_ext = APIRouter()
# Register static files path
my_extension_static_files = [
{
"path": "/my_extension/static",
"name": "my_extension_static",
}
]
def my_extension_start():
"""Called when the extension starts."""
pass
def my_extension_stop():
"""Called when the extension stops."""
passAPI endpoints (views_api.py)
python
from http import HTTPStatus
from fastapi import APIRouter, Depends
from lnbits.decorators import (
require_admin_key,
require_invoice_key,
WalletTypeInfo,
)
from .crud import create_item, get_items
from .models import CreateItemRequest, Item
router = APIRouter()
@router.get("/api/v1/items")
async def api_get_items(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[Item]:
return await get_items(wallet.wallet.id)
@router.post("/api/v1/items", status_code=HTTPStatus.CREATED)
async def api_create_item(
data: CreateItemRequest,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Item:
return await create_item(wallet.wallet.id, data)Models (models.py)
python
from pydantic import BaseModel
class CreateItemRequest(BaseModel):
name: str
price: int
description: str = ""
class Item(BaseModel):
id: str
wallet: str
name: str
price: int
description: strDatabase operations (crud.py)
python
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash
from .models import CreateItemRequest, Item
db = Database("ext_my_extension")
async def create_item(wallet_id: str, data: CreateItemRequest) -> Item:
item_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO my_extension.items (id, wallet, name, price, description)
VALUES (:id, :wallet, :name, :price, :description)
""",
{
"id": item_id,
"wallet": wallet_id,
"name": data.name,
"price": data.price,
"description": data.description,
},
)
item = await get_item(item_id)
assert item, "Created item not found"
return item
async def get_item(item_id: str) -> Item | None:
row = await db.fetchone(
"SELECT * FROM my_extension.items WHERE id = :id",
{"id": item_id},
)
return Item(**row) if row else None
async def get_items(wallet_id: str) -> list[Item]:
rows = await db.fetchall(
"SELECT * FROM my_extension.items WHERE wallet = :wallet",
{"wallet": wallet_id},
)
return [Item(**row) for row in rows]Migrations (migrations.py)
python
async def m001_initial(db):
"""Create initial tables."""
await db.execute(
"""
CREATE TABLE my_extension.items (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
price INTEGER NOT NULL,
description TEXT DEFAULT ''
);
"""
)
async def m002_add_created_at(db):
"""Add created_at column."""
await db.execute(
"""
ALTER TABLE my_extension.items
ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
"""
)Frontend template
html
{% extends "base.html" %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12">
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">My Extension</h5>
</q-card-section>
<q-card-section>
<q-table
:rows="items"
:columns="columns"
row-key="id"
/>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#vue',
data() {
return {
items: [],
columns: [
{ name: 'name', label: 'Name', field: 'name' },
{ name: 'price', label: 'Price (sats)', field: 'price' },
]
}
},
async created() {
const { data } = await LNbits.api.request(
'GET',
'/my_extension/api/v1/items',
this.g.user.wallets[0].inkey
)
this.items = data
}
})
</script>
{% endblock %}Listening for payments
Register an invoice listener to react to payments:
python
from lnbits.tasks import register_invoice_listener
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, "ext_my_extension")
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment):
# Check if this payment is for your extension
if payment.extra.get("tag") != "my_extension":
return
# Handle the payment
await process_order(payment)Testing with FakeWallet
During development, use FakeWallet for instant, reliable testing:
bash
LNBITS_BACKEND_WALLET_CLASS=FakeWallet
FAKE_WALLET_SECRET=test-secretDependencies
DO NOT ADD NEW DEPENDENCIES
Use the packages already available in LNbits' pyproject.toml. Adding a new dependency is time-consuming and uncertain, and may result in your extension NOT being made available to others.
If your extension absolutely requires a new package:
- Check
pyproject.tomlfor an existing package that covers your need - Open a GitHub issue explaining why it's necessary
- Add it to
pyproject.tomland test with all three installers:
bash
# Must pass all three:
uv pip install -e "." # uv
poetry install # Poetry
nix build .#checks.x86_64-linux.vmTest # NixSee Contributing - Adding new dependencies for the full process.
Publishing
- Host your extension on GitHub
- Create a release with the extension files as a zip
- Add your repository to an extension manifest
- Users can install directly from the LNbits UI
For full deployment details, see the Deploying Extensions guide, which covers:
- Local development deployment - test by dropping folders on disk
- Remote manifest distribution - publish via manifest files
- Official registry submission - get listed on the default marketplace
- Monetization - charge for extensions via Lightning payments
- Auto-install - pre-configure extensions for Docker and automated deployments
Related Pages
- Deploying Extensions - packaging, distribution, and installation
- Contributing
- Database & Migrations
- Decorators & Auth
- Background Tasks