@aphrody/sql
v1.4.7
Published
Shared Bun database mechanics for Aphrody: env/URL resolution, Bun.SQL and Redis factories, bun:sqlite open policy with optional sqlite-vec
Readme
@aphrody/sql
Shared Bun database mechanics for Aphrody. The package implements decisions
D6, D7, D9 and D10 of
ADR sql-convergence. The
Rust counterpart is the aphrody-sql crate (crates/infra/sql), which applies
the same SQLite policy.
The package exports TypeScript sources. It has no ORM: consumers keep theirs
(Drizzle in Shenron's @shenron/db, Kysely in @rosegriffon/db).
Modules
| Subpath | Runtime | Contents |
| ----------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| @aphrody/sql/env | any (no bun, node: or DOM import) | env/URL resolution, redactUrl, pooler and loopback detection, dotenv lastAssignment, resolvePoolSize |
| @aphrody/sql/postgres | Bun | sqlOptions, createSql, withTransaction, transactionMode over Bun.SQL |
| @aphrody/sql/redis | importable anywhere; client creation requires Bun | createRedisClient, redisOptions, resolveRedisUrl over Bun's RedisClient |
| @aphrody/sql/sqlite | Bun | openSqlite, closeSqlite, loadSqliteVec, sqliteVecVersion, vacuumInto over bun:sqlite |
| @aphrody/sql | Bun | re-exports all of the above |
Import a subpath rather than the root, so that browser or Next bundles never
pull in bun:sqlite.
Environment naming (D9)
<DOMAIN>_DATABASE_URLis the direct URL. Use it for migrations,COPY,LISTENand advisory locks.<DOMAIN>_DATABASE_URL_POOLEDis the runtime URL behind a pooler.- Existing contract names are kept, never renamed. Pass them as
fallbacks.
import { requireDatabaseUrl, redactUrl } from "@aphrody/sql/env";
// Tries APHRODY_TRAINING_DATABASE_URL_POOLED, APHRODY_TRAINING_DATABASE_URL, then DATABASE_URL.
const { url, name } = requireDatabaseUrl({
prefix: "APHRODY_TRAINING",
pooled: true,
fallbacks: ["DATABASE_URL"],
});
console.log(`database from ${name}: ${redactUrl(url)}`);Resolution trims values. It skips empty strings, the literals
"undefined"/"null" and dotenvx payloads that were never decrypted
(eyJ2Ijo…). If no URL resolves, the error names the variables tried and never
their values.
PostgreSQL (Bun.SQL)
import { createSql, withTransaction } from "@aphrody/sql/postgres";
const sql = createSql(url, { applicationName: "aphrody-train" });
await withTransaction(sql, async tx => tx`SELECT 1`, {
readOnly: true,
statementTimeoutMs: 5000,
});- Pool defaults:
max4,idleTimeout30 s,maxLifetime1800 s,connectionTimeout10 s. tls: "auto"(default):- A non-loopback host gets
"require". - Loopback hosts and Unix sockets (
?host=/run/postgresql) stay plain. - A URL that already sets
sslmode,sslortlskeeps its own mode. falseopts out; any Bun TLS value can be passed explicitly.
- A non-loopback host gets
prepare: "auto"(default): setsprepare: falsebehind a transaction pooler, which is any of:- PgBouncer on port 6432;
- Supavisor on port 6543;
- a Neon
-pooler.host; - a URL with
?pgbouncer=true.
"inherit"leaves either setting to the URL and to Bun's defaults.Bun.SQLhas noCOPY. Use postgres.js or a Rust path for bulk ingest.
Redis
import { createRedisClient } from "@aphrody/sql/redis";
const redis = createRedisClient(); // REDIS_URL, VALKEY_URL, then redis://127.0.0.1:6379- The defaults are spelled out and equal Bun's own:
connectionTimeout10 000 ms,autoReconnect,maxRetries20, offline queue and auto-pipelining. Caller options override them. - Once
maxRetriesis exhausted, a Bun client stays closed. Long-lived services should rebuild it (ascreateResilientRedisof Shenron's@shenron/db/redisdoes). - Redis is a cache or session store only (D1).
SQLite (bun:sqlite)
openSqlite(path, { readonly, vec, busyTimeoutMs, create, strict, safeIntegers })
applies the same pragmas as the Rust crate:
| Pragma | Value | Note |
| -------------- | --------- | --------------------------------------------- |
| busy_timeout | 5000 | set first |
| journal_mode | WAL | skipped for read-only and in-memory databases |
| foreign_keys | ON | |
| temp_store | MEMORY | |
| optimize | 0x10002 | at open; skipped for read-only connections |
closeSqlite(db)runsPRAGMA optimizebefore it closes the connection.vec: trueloads sqlite-vec through the npmsqlite-vecpackage (load(db), version^0.1.9), which providesvec0andvec_version(). On macOS, callDatabase.setCustomSQLite()before opening, because the system SQLite cannot load extensions.vacuumInto(db, path)makes a consistent online copy withVACUUM INTO. This is the nightly SQLite backup path (D12). SQLite refuses to write over an existing, non-empty target.
Consumers
@rosegriffon/db(iecode/packages/rg/db):bun-sqlbuilds its pool withcreateSqlandresolvePoolSize, usingtls/prepare: "inherit".redisbuilds its client withcreateRedisClientand logs throughredactUrl.envusesisUsableEnvValue.- The Next site lists
@aphrody/sqlintranspilePackages.
@shenron/db(packages/dbof aphrody-labs/shenron):bun-sqlbuilds its pool withcreateSql, usingtls: "inherit"with every other option explicit.envre-exportslastAssignment.redisbuilds its default client withcreateRedisClient.
Both packages keep their exported names, signatures and behaviour; their existing tests cover these contracts.
Checks
bun test # in packages/infra/sql (no network)
bun check -p tsconfig.json --noEmit
bun lint -c ../../docs/aphrody/.oxlintrc.json src test