Skip to content

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 JavaScript

config.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."""
    pass

API 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: str

Database 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-secret

Dependencies ​

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:

  1. Check pyproject.toml for an existing package that covers your need
  2. Open a GitHub issue explaining why it's necessary
  3. Add it to pyproject.toml and 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  # Nix

See Contributing - Adding new dependencies for the full process.

Publishing ​

  1. Host your extension on GitHub
  2. Create a release with the extension files as a zip
  3. Add your repository to an extension manifest
  4. Users can install directly from the LNbits UI

For full deployment details, see the Deploying Extensions guide, which covers: