https://github.com/rsclarke/certmagic-sqlite
SQLite storage backend for caddyserver/certmagic
https://github.com/rsclarke/certmagic-sqlite
certmagic sqlite
Last synced: about 2 months ago
JSON representation
SQLite storage backend for caddyserver/certmagic
- Host: GitHub
- URL: https://github.com/rsclarke/certmagic-sqlite
- Owner: rsclarke
- License: mit
- Created: 2026-01-28T01:15:24.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-18T13:26:41.000Z (3 months ago)
- Last Synced: 2026-03-18T21:47:21.425Z (3 months ago)
- Topics: certmagic, sqlite
- Language: Go
- Homepage:
- Size: 41 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# certmagic-sqlite
A SQLite storage backend for [CertMagic](https://github.com/caddyserver/certmagic). Pure Go, no CGO required.
## Features
- Implements `certmagic.Storage` and `certmagic.Locker` interfaces
- Pure Go SQLite driver ([modernc.org/sqlite](https://pkg.go.dev/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
```bash
go get github.com/rsclarke/certmagic-sqlite
```
## Usage
### Standalone Database
Use `New()` when CertMagic should manage its own database file:
```go
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:
```go
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:
```go
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:
```go
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.) |
## Recommended SQLite Settings for `NewWithDB`
When using `NewWithDB()`, you are responsible for configuring the database connection. Here are the recommended settings:
### Journal Mode: WAL
```go
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
```go
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
```go
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
```go
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
```go
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:
```sql
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.