certmagicsqlite

package module
v0.0.0-...-f3c85e0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 6, 2026 License: MIT Imports: 12 Imported by: 0

README

certmagic-sqlite

A SQLite storage backend for CertMagic. Pure Go, no CGO required.

Features

  • Implements certmagic.Storage and certmagic.Locker interfaces
  • Pure Go SQLite driver (modernc.org/sqlite) — no CGO
  • Supports shared database connections for applications already using SQLite
  • Automatic schema migrations
  • Distributed locking with expiration-based stale lock detection

Installation

go get github.com/rsclarke/certmagic-sqlite

Usage

Standalone Database

Use New() when CertMagic should manage its own database file:

package main

import (
    "github.com/caddyserver/certmagic"
    "github.com/rsclarke/certmagic-sqlite"
)

func main() {
    storage, err := certmagicsqlite.New("/path/to/certs.db")
    if err != nil {
        log.Fatal(err)
    }
    defer storage.Close()

    certmagic.Default.Storage = storage

    // Use certmagic...
}

New() automatically configures:

  • WAL journal mode (for better concurrency)
  • synchronous=NORMAL (safe balance of durability and performance)
  • busy_timeout=5000 (5 second wait on lock contention)
Shared Database

Use NewWithDB() when you want to store certificates in an existing application database:

package main

import (
    "database/sql"

    "github.com/caddyserver/certmagic"
    "github.com/rsclarke/certmagic-sqlite"
    _ "modernc.org/sqlite"
)

func main() {
    // Open your application database
    db, err := sql.Open("sqlite", "/path/to/app.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Configure recommended PRAGMAs (see below)
    db.Exec("PRAGMA journal_mode=WAL")
    db.Exec("PRAGMA busy_timeout=5000")

    // Create your application tables
    db.Exec("CREATE TABLE IF NOT EXISTS users (...)")

    // Create CertMagic storage (runs schema migrations automatically)
    storage, err := certmagicsqlite.NewWithDB(db)
    if err != nil {
        log.Fatal(err)
    }
    defer storage.Close() // No-op; doesn't close the shared db

    certmagic.Default.Storage = storage
}

Configuration Options

Both constructors accept functional options:

storage, err := certmagicsqlite.New("/path/to/certs.db",
    certmagicsqlite.WithLockTTL(5*time.Minute),      // Lock expiration (default: 2 minutes)
    certmagicsqlite.WithQueryTimeout(10*time.Second), // Query timeout (default: 3 seconds)
)
Option Default Description
WithLockTTL(d) 2 minutes Duration after which locks expire and can be stolen
WithQueryTimeout(d) 3 seconds Timeout for individual database queries
WithOwnerID(id) Random UUID Identifier for lock ownership (see below)
Lock Owner ID

By default, a random UUID is generated each time the storage is created. This means after a restart, the application cannot clean up its own stale locks — it must wait for them to expire.

For faster recovery after restarts, provide a stable owner ID:

import "os"

hostname, _ := os.Hostname()
storage, err := certmagicsqlite.New("/path/to/certs.db",
    certmagicsqlite.WithOwnerID(hostname),
)
Deployment Recommended ownerID
Single instance Hostname or fixed string
Multiple instances Unique per instance (hostname, pod name, etc.)

When using NewWithDB(), you are responsible for configuring the database connection. Here are the recommended settings:

Journal Mode: WAL
db.Exec("PRAGMA journal_mode=WAL")

WAL (Write-Ahead Logging) provides:

  • Better concurrency (readers don't block writers)
  • Improved performance for mixed read/write workloads
  • Crash resilience

Note: WAL is not supported for :memory: databases.

Busy Timeout
db.Exec("PRAGMA busy_timeout=5000")  // 5000ms = 5 seconds

Configures how long SQLite waits when the database is locked by another connection. Without this, you may see "database is locked" errors under contention.

Synchronous Mode
db.Exec("PRAGMA synchronous=NORMAL")

Options:

  • FULL (default): Safest, slowest — syncs after every transaction
  • NORMAL: Safe with WAL — syncs less frequently, good performance
  • OFF: Fastest, but risks corruption on power loss

NORMAL is recommended when using WAL mode.

Connection Pool Settings
db.SetMaxOpenConns(1)  // Serialize all writes
db.SetMaxIdleConns(1)  // Keep connection warm

SQLite only supports one writer at a time. Setting MaxOpenConns(1) prevents "database is locked" errors by serializing access at the Go level.

Complete Example
db, _ := sql.Open("sqlite", "/path/to/app.db")

// Recommended settings
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.Exec("PRAGMA journal_mode=WAL")
db.Exec("PRAGMA synchronous=NORMAL")
db.Exec("PRAGMA busy_timeout=5000")

storage, _ := certmagicsqlite.NewWithDB(db)

Schema

The following tables are created automatically:

CREATE TABLE IF NOT EXISTS certmagic_data (
    key TEXT PRIMARY KEY,
    value BLOB NOT NULL,
    modified INTEGER NOT NULL  -- Unix timestamp (milliseconds)
);

CREATE TABLE IF NOT EXISTS certmagic_locks (
    name TEXT PRIMARY KEY,
    expires_at INTEGER NOT NULL,  -- Unix timestamp (milliseconds)
    owner_id TEXT NOT NULL
);

These tables coexist safely with your application tables.

When to Use SQLite vs Other Backends

SQLite is ideal for:

  • Single-node deployments
  • Embedded applications or appliances
  • Simplicity (no external database server)
  • Applications already using SQLite

Consider PostgreSQL/Redis/Consul for:

  • Multi-node clusters sharing certificates
  • High-availability deployments
  • Distributed locking across multiple servers

SQLite storage provides the same single-node guarantees as the default filesystem storage, with added benefits of atomic transactions and single-file backup.

Documentation

Overview

Package certmagicsqlite provides a SQLite-backed storage implementation for CertMagic. It implements the certmagic.Storage and certmagic.Locker interfaces using a pure Go SQLite driver (no CGO required).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*SQLiteStorage)

Option configures a SQLiteStorage instance.

func WithLockTTL

func WithLockTTL(d time.Duration) Option

WithLockTTL sets the duration after which locks expire. Default is 2 minutes.

func WithOwnerID

func WithOwnerID(id string) Option

WithOwnerID sets a custom owner identifier for distributed locking. By default, a random UUID is generated on each instantiation.

Providing a stable ownerID (e.g., hostname, instance ID) allows the application to clean up its own stale locks after a restart, rather than waiting for them to expire.

For single-instance deployments, use a stable ID like hostname. For multi-instance deployments, ensure each instance has a unique ID.

func WithQueryTimeout

func WithQueryTimeout(d time.Duration) Option

WithQueryTimeout sets the timeout for database queries. Default is 3 seconds.

type SQLiteStorage

type SQLiteStorage struct {
	// contains filtered or unexported fields
}

SQLiteStorage implements certmagic.Storage and certmagic.Locker using a SQLite database for persistence.

func New

func New(dsn string, opts ...Option) (*SQLiteStorage, error)

New creates a new SQLiteStorage instance. The dsn parameter should be a path to the SQLite database file, or ":memory:" for an in-memory database. The storage will manage the database connection and close it when Close() is called. Recommended PRAGMAs (WAL, synchronous=NORMAL, busy_timeout) are applied automatically.

func NewWithDB

func NewWithDB(db *sql.DB, opts ...Option) (*SQLiteStorage, error)

NewWithDB creates a new SQLiteStorage instance using an existing database connection. This allows sharing a SQLite database with other parts of your application. The caller is responsible for closing the database connection. Schema migrations will be run automatically.

func (*SQLiteStorage) Close

func (s *SQLiteStorage) Close() error

Close closes the database connection if it was opened by New(). If the storage was created with NewWithDB(), this is a no-op.

func (*SQLiteStorage) Delete

func (s *SQLiteStorage) Delete(ctx context.Context, key string) error

Delete removes the key and all keys with the same prefix.

func (*SQLiteStorage) Exists

func (s *SQLiteStorage) Exists(ctx context.Context, key string) bool

Exists returns true if the key exists (either as a file or prefix).

func (*SQLiteStorage) List

func (s *SQLiteStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error)

List returns all keys with the given prefix. If recursive is false, only direct children are returned. Returns fs.ErrNotExist if no keys match.

func (*SQLiteStorage) Load

func (s *SQLiteStorage) Load(ctx context.Context, key string) ([]byte, error)

Load retrieves the value at the given key. Returns fs.ErrNotExist if the key does not exist.

func (*SQLiteStorage) Lock

func (s *SQLiteStorage) Lock(ctx context.Context, name string) error

Lock acquires a named lock, blocking until the lock is acquired or the context is cancelled.

func (*SQLiteStorage) Stat

func (s *SQLiteStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error)

Stat returns information about the key. Returns fs.ErrNotExist if the key does not exist.

func (*SQLiteStorage) Store

func (s *SQLiteStorage) Store(ctx context.Context, key string, value []byte) error

Store saves the value at the given key.

func (*SQLiteStorage) Unlock

func (s *SQLiteStorage) Unlock(ctx context.Context, name string) error

Unlock releases a named lock. Returns fs.ErrNotExist if the lock was not held by this owner.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL