data-access-abstraction
Skill37 starsChanged 4 months agoMITView on GitHub
Data access abstraction patterns for apps that need to swap between local databases (SQLite) and cloud databases (Cosmos DB, PostgreSQL) without changing application code. Covers Node.js/TypeScript, Python/FastAPI, and .NET. Use when building apps that run locally with SQLite and deploy to Azure with Cosmos DB or PostgreSQL.
- Reads credentials
1 other project uses a version of this.
Adopted by
Most recently taken up by
- microsoft/agentic-journeysword for word, first seen Sep 30, 2026
Other versions1 project uses a version of this.
Word for word · 1
- microsoft/agentic-journeys★ 5100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository8 other files it gives its agents.
- AGENTS.mdAGENTS.md
- container-apps-deploymentSkill
- grafana-azureSkill
- journey-runnerSkill
- journey-templateSkill
- journey-test-harnessSkill
- n8n-azureSkill
- superset-azureSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 39
All languages use the same convention:
- Very specific: line 109
Use better-sqlite3 (synchronous API, fast).
- Very specific: line 176
Use pg (node-postgres). PostgreSQL supports native arrays and JSONB, so it sits between SQLite and Cosmos in terms of data handling.
- Very specific: line 215
PostgreSQL-specific: Use TEXT[] for arrays (native), JSONB for nested objects, and $1 parameterized queries.
- Very specific: line 238
Use Protocol (structural typing) or ABC (nominal typing).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Data Access Abstraction
Build APIs with swappable data layers using the repository pattern. Develop locally with SQLite, deploy to Azure with Cosmos DB or PostgreSQL — same route code, different backend. Works in any language.
When to Use This Skill
- Building an API that needs to run locally (SQLite) and in Azure (Cosmos DB or PostgreSQL)
- Adding a new data provider to an existing app without changing routes or business logic
- Migrating from one database to another incrementally
- Any project following the AIMarket journey pattern (local dev → cloud deploy)
Pattern Overview
The pattern is the same regardless of language:
Routes/Controllers → Repository Interfaces → Factory → Implementations
│
├── SQLite (local dev)
├── Cosmos DB (Azure deploy)
├── PostgreSQL (Azure deploy)
└── In-memory (testing)Three rules:
- Define repository interfaces (or abstract classes / protocols) per entity
- Routes depend only on the interfaces — never import a database client directly
- A factory reads a config value (
DATA_PROVIDER) and returns the right implementation
Adding a new database means writing a new implementation file. Zero changes to routes.
Environment Variable
All languages use the same convention:
DATA_PROVIDER=sqlite # Local development (default)
DATA_PROVIDER=cosmos # Azure Cosmos DB
DATA_PROVIDER=postgres # Azure PostgreSQLNode.js / TypeScript
Repository Interfaces
// data/interfaces.ts
export interface IProductRepository {
getAll(params: {
page: number; pageSize: number;
category?: string; minPrice?: number; maxPrice?: number;
}): Promise<{ data: Product[]; totalCount: number }>;
getById(id: string): Promise<Product | null>;
create(input: CreateProductInput): Promise<Product>;
update(id: string, fields: Partial<Product>): Promise<Product | null>;
}
export interface IOrderRepository {
create(input: CreateOrderInput): Promise<Order>;
getById(id: string): Promise<Order | null>;
getByUserId(userId: string, page: number, pageSize: number): Promise<{ data: Order[]; totalCount: number }>;
}
export interface IUserRepository {
create(input: CreateUserInput): Promise<User>;
getById(id: string): Promise<User | null>;
getByEmail(email: string): Promise<User | null>;
}
export interface DataStore {
products: IProductRepository;
orders: IOrderRepository;
users: IUserRepository;
close?(): void;
}Factory
// data/store.ts
export async function createStore(): Promise<DataStore> {
const provider = process.env.DATA_PROVIDER || 'sqlite';
switch (provider) {
case 'sqlite':
return (await import('./sqlite.js')).createSqliteStore();
case 'cosmos':
return (await import('./cosmos.js')).createCosmosStore();
case 'postgres':
return (await import('./postgres.js')).createPostgresStore();
default:
throw new Error(`Unknown DATA_PROVIDER: ${provider}`);
}
}SQLite Implementation
Use better-sqlite3 (synchronous API, fast). Wrap sync calls in Promise.resolve() or make routes await — sync values resolve immediately.
// data/sqlite.ts
import Database from 'better-sqlite3';
export function createSqliteStore(dbPath = 'app.db'): DataStore {
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// Create tables, seed if empty...
return {
products: {
async getAll({ page, pageSize, category, minPrice, maxPrice }) {
let where = 'WHERE status = ?';
const params: any[] = ['active'];
if (category) { where += ' AND category = ?'; params.push(category); }
if (minPrice != null) { where += ' AND price >= ?'; params.push(minPrice); }
if (maxPrice != null) { where += ' AND price <= ?'; params.push(maxPrice); }
const { c: totalCount } = db.prepare(`SELECT COUNT(*) as c FROM products ${where}`).get(...params);
const rows = db.prepare(`SELECT * FROM products ${where} LIMIT ? OFFSET ?`)
.all(...params, pageSize, (page - 1) * pageSize);
return { data: rows.map(parseRow), totalCount };
},
// ...
},
close: () => db.close(),
};
}SQLite-specific: Store arrays as JSON strings (JSON.stringify(tags)), parse on read. Use a junction table for order items.
Cosmos DB Implementation
// data/cosmos.ts
import { CosmosClient } from '@azure/cosmos';
export function createCosmosStore(): DataStore {
const client = new CosmosClient({ endpoint: process.env.COSMOS_ENDPOINT!, key: process.env.COSMOS_KEY! });
const db = client.database(process.env.COSMOS_DATABASE || 'aimarket');
return {
products: {
async getAll({ page, pageSize, category, minPrice, maxPrice }) {
let query = 'SELECT * FROM c WHERE c.status = @status';
const parameters = [{ name: '@status', value: 'active' }];
if (category) { query += ' AND c.category = @cat'; parameters.push({ name: '@cat', value: category }); }
// ... build query
const { resources } = await db.container('products').items.query({ query, parameters }).fetchAll();
return { data: resources.slice((page-1)*pageSize, page*pageSize), totalCount: resources.length };
},
// ...
},
};
}Cosmos-specific: Arrays and objects are native JSON — no serialization. Embed order items in the order document.
PostgreSQL Implementation
Use pg (node-postgres). PostgreSQL supports native arrays and JSONB, so it sits between SQLite and Cosmos in terms of data handling.
// data/postgres.ts
import pg from 'pg';
export function createPostgresStore(): DataStore {
const pool = new pg.Pool({
connectionString: process.env.POSTGRES_CONNECTION_STRING,
ssl: process.env.POSTGRES_SSL === 'true' ? { rejectUnauthorized: false } : undefined,
});
return {
products: {
async getAll({ page, pageSize, category, minPrice, maxPrice }) {
let where = 'WHERE status = $1';
const params: any[] = ['active'];
let idx = 2;
if (category) { where += ` AND category = $${idx++}`; params.push(category); }
if (minPrice != null) { where += ` AND price >= $${idx++}`; params.push(minPrice); }
if (maxPrice != null) { where += ` AND price <= $${idx++}`; params.push(maxPrice); }
const countRes = await pool.query(`SELECT COUNT(*) FROM products ${where}`, params);
const totalCount = parseInt(countRes.rows[0].count);
const dataRes = await pool.query(
`SELECT * FROM products ${where} LIMIT $${idx++} OFFSET $${idx}`,
[...params, pageSize, (page - 1) * pageSize]
);
return { data: dataRes.rows, totalCount };
},
// ...
},
close: () => pool.end(),
};
}PostgreSQL-specific: Use TEXT[] for arrays (native), JSONB for nested objects, and $1 parameterized queries. Azure PostgreSQL requires ssl: { rejectUnauthorized: false }.
Routes
// routes/products.ts — depends only on DataStore interface
export function productRoutes(store: DataStore): Router {
const router = Router();
router.get('/', async (req, res) => {
const result = await store.products.getAll({ page: 1, pageSize: 20 });
res.json(result);
});
return router;
}Python / FastAPI
Repository Interfaces
Use Protocol (structural typing) or ABC (nominal typing). Protocol is more Pythonic.
# data/interfaces.py
from typing import Protocol
from models import Product, Order, User
class ProductRepository(Protocol):
def get_all(self, *, page: int = 1, page_size: int = 20,
category: str | None = None, min_price: float | None = None,
max_price: float | None = None) -> dict:
"""Returns { 'data': list[Product], 'total_count': int }"""
...
def get_by_id(self, id: str) -> Product | None: ...
def create(self, data: dict) -> Product: ...
def update(self, id: str, fields: dict) -> Product | None: ...
class OrderRepository(Protocol):
def create(self, data: dict) -> Order: ...
def get_by_id(self, id: str) -> Order | None: ...
def get_by_user_id(self, user_id: str, page: int, page_size: int) -> dict: ...
class UserRepository(Protocol):
def create(self, data: dict) -> User: ...
def get_by_id(self, id: str) -> User | None: ...
def get_by_email(self, email: str) -> User | None: ...
class DataStore(Protocol):
products: ProductRepository
orders: OrderRepository
users: UserRepositoryFactory
# data/store.py
import os
def create_store() -> DataStore:
provider = os.getenv("DATA_PROVIDER", "sqlite")
if provider == "sqlite":
from data.sqlite import create_sqlite_store
return create_sqlite_store()
elif provider == "cosmos":
from data.cosmos import create_cosmos_store
return create_cosmos_store()
elif provider == "postgres":
from data.postgres import create_postgres_store
return create_postgres_store()
else:
raise ValueError(f"Unknown DATA_PROVIDER: {provider}")SQLite Implementation
# data/sqlite.py
import sqlite3, json, uuid
from models import Product
class SqliteProductRepository:
def __init__(self, db: sqlite3.Connection):
self.db = db
def get_all(self, *, page=1, page_size=20, category=None, min_price=None, max_price=None):
where, params = "WHERE status = ?", ["active"]
if category:
where += " AND category = ?"; params.append(category)
if min_price is not None:
where += " AND price >= ?"; params.append(min_price)
if max_price is not None:
where += " AND price <= ?"; params.append(max_price)
total = self.db.execute(f"SELECT COUNT(*) FROM products {where}", params).fetchone()[0]
rows = self.db.execute(
f"SELECT * FROM products {where} LIMIT ? OFFSET ?",
params + [page_size, (page - 1) * page_size]
).fetchall()
return {"data": [self._parse(r) for r in rows], "total_count": total}
def _parse(self, row) -> Product:
d = dict(row)
d["tags"] = json.loads(d["tags"]) # JSON string → list
return Product(**d)
def create_sqlite_store(db_path="app.db"):
db = sqlite3.connect(db_path)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA foreign_keys=ON")
# Create tables, seed if empty...
return SqliteDataStore(
products=SqliteProductRepository(db),
orders=SqliteOrderRepository(db),
users=SqliteUserRepository(db),
)Cosmos DB Implementation
# data/cosmos.py
from azure.cosmos import CosmosClient
import os
class CosmosProductRepository:
def __init__(self, container):
self.container = container
def get_all(self, *, page=1, page_size=20, category=None, min_price=None, max_price=None):
query = "SELECT * FROM c WHERE c.status = @status"
params = [{"name": "@status", "value": "active"}]
if category:
query += " AND c.category = @cat"
params.append({"name": "@cat", "value": category})
# ... build query
items = list(self.container.query_items(query=query, parameters=params, enable_cross_partition_query=True))
start = (page - 1) * page_size
return {"data": items[start:start+page_size], "total_count": len(items)}
def create_cosmos_store():
client = CosmosClient(os.environ["COSMOS_ENDPOINT"], os.environ["COSMOS_KEY"])
db = client.get_database_client(os.getenv("COSMOS_DATABASE", "aimarket"))
return CosmosDataStore(
products=CosmosProductRepository(db.get_container_client("products")),
orders=CosmosOrderRepository(db.get_container_client("orders")),
users=CosmosUserRepository(db.get_container_client("users")),
)FastAPI Routes
# routes/products.py — depends only on DataStore protocol
from fastapi import APIRouter, Depends
from data.store import create_store
router = APIRouter(prefix="/api/products")
@router.get("/")
def list_products(page: int = 1, page_size: int = 20, category: str | None = None,
store: DataStore = Depends(create_store)):
result = store.products.get_all(page=page, page_size=page_size, category=category)
return {**result, "page": page, "page_size": page_size}.NET / C# (Minimal APIs or Controllers)
Repository Interfaces
// Data/Interfaces.cs
public interface IProductRepository
{
Task<(List<Product> Data, int TotalCount)> GetAllAsync(
int page = 1, int pageSize = 20,
string? category = null, decimal? minPrice = null, decimal? maxPrice = null);
Task<Product?> GetByIdAsync(string id);
Task<Product> CreateAsync(CreateProductInput input);
Task<Product?> UpdateAsync(string id, UpdateProductInput input);
}
public interface IOrderRepository
{
Task<Order> CreateAsync(CreateOrderInput input);
Task<Order?> GetByIdAsync(string id);
Task<(List<Order> Data, int TotalCount)> GetByUserIdAsync(string userId, int page, int pageSize);
}
public interface IUserRepository
{
Task<User> CreateAsync(CreateUserInput input);
Task<User?> GetByIdAsync(string id);
Task<User?> GetByEmailAsync(string email);
}Dependency Injection (Factory)
// Program.cs
var provider = builder.Configuration["DATA_PROVIDER"] ?? "sqlite";
if (provider == "sqlite")
{
builder.Services.AddSingleton<IProductRepository, SqliteProductRepository>();
builder.Services.AddSingleton<IOrderRepository, SqliteOrderRepository>();
builder.Services.AddSingleton<IUserRepository, SqliteUserRepository>();
}
else if (provider == "cosmos")
{
builder.Services.AddSingleton<IProductRepository, CosmosProductRepository>();
builder.Services.AddSingleton<IOrderRepository, CosmosOrderRepository>();
builder.Services.AddSingleton<IUserRepository, CosmosUserRepository>();
}
else if (provider == "postgres")
{
builder.Services.AddSingleton<IProductRepository, PostgresProductRepository>();
builder.Services.AddSingleton<IOrderRepository, PostgresOrderRepository>();
builder.Services.AddSingleton<IUserRepository, PostgresUserRepository>();
}.NET uses built-in DI instead of a manual factory. The pattern is the same: register implementations based on config, inject interfaces into routes.
SQLite Implementation
// Data/SqliteProductRepository.cs
using Microsoft.Data.Sqlite;
using System.Text.Json;
public class SqliteProductRepository : IProductRepository
{
private readonly SqliteConnection _db;
public SqliteProductRepository(IConfiguration config)
{
_db = new SqliteConnection(config.GetConnectionString("Sqlite") ?? "Data Source=app.db");
_db.Open();
// Create tables, seed if empty...
}
public async Task<(List<Product> Data, int TotalCount)> GetAllAsync(
int page = 1, int pageSize = 20,
string? category = null, decimal? minPrice = null, decimal? maxPrice = null)
{
var where = "WHERE status = 'active'";
if (category != null) where += $" AND category = @category";
if (minPrice != null) where += $" AND price >= @minPrice";
if (maxPrice != null) where += $" AND price <= @maxPrice";
// Execute count + select queries...
// Parse tags from JSON string: JsonSerializer.Deserialize<string[]>(row["tags"])
}
}Cosmos DB Implementation
// Data/CosmosProductRepository.cs
using Microsoft.Azure.Cosmos;
public class CosmosProductRepository : IProductRepository
{
private readonly Container _container;
public CosmosProductRepository(CosmosClient client, IConfiguration config)
{
var db = client.GetDatabase(config["COSMOS_DATABASE"] ?? "aimarket");
_container = db.GetContainer("products");
}
public async Task<(List<Product> Data, int TotalCount)> GetAllAsync(
int page = 1, int pageSize = 20,
string? category = null, decimal? minPrice = null, decimal? maxPrice = null)
{
var query = new QueryDefinition("SELECT * FROM c WHERE c.status = @status")
.WithParameter("@status", "active");
// Add filters, execute, paginate...
}
}Minimal API Routes
// Routes depend only on interfaces — injected via DI
app.MapGet("/api/products", async (IProductRepository repo,
int page = 1, int pageSize = 20, string? category = null) =>
{
var (data, total) = await repo.GetAllAsync(page, pageSize, category);
return Results.Ok(new { data, page, pageSize, totalCount = total });
});Java / Spring Boot
Repository Interfaces
// data/ProductRepository.java
public interface ProductRepository {
PaginatedResult<Product> getAll(int page, int pageSize,
String category, Double minPrice, Double maxPrice);
Optional<Product> getById(String id);
Product create(CreateProductInput input);
Optional<Product> update(String id, UpdateProductInput input);
}
// data/OrderRepository.java
public interface OrderRepository {
Order create(CreateOrderInput input);
Optional<Order> getById(String id);
PaginatedResult<Order> getByUserId(String userId, int page, int pageSize);
}
// data/UserRepository.java
public interface UserRepository {
User create(CreateUserInput input);
Optional<User> getById(String id);
Optional<User> getByEmail(String email);
}Factory via Spring Profiles
Spring profiles replace the manual factory. Set DATA_PROVIDER as the active profile:
# application.yml
spring:
profiles:
active: ${DATA_PROVIDER:sqlite}// data/sqlite/SqliteProductRepository.java
@Repository
@Profile("sqlite")
public class SqliteProductRepository implements ProductRepository {
private final JdbcTemplate jdbc;
public SqliteProductRepository(DataSource dataSource) {
this.jdbc = new JdbcTemplate(dataSource);
}
@Override
public PaginatedResult<Product> getAll(int page, int pageSize,
String category, Double minPrice, Double maxPrice) {
var where = new StringBuilder("WHERE status = 'active'");
var params = new ArrayList<>();
if (category != null) { where.append(" AND category = ?"); params.add(category); }
if (minPrice != null) { where.append(" AND price >= ?"); params.add(minPrice); }
if (maxPrice != null) { where.append(" AND price <= ?"); params.add(maxPrice); }
int total = jdbc.queryForObject(
"SELECT COUNT(*) FROM products " + where, Integer.class, params.toArray());
var rows = jdbc.query(
"SELECT * FROM products " + where + " LIMIT ? OFFSET ?",
this::mapRow,
Stream.concat(params.stream(), Stream.of(pageSize, (page - 1) * pageSize)).toArray());
return new PaginatedResult<>(rows, total);
}
private Product mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Product(
rs.getString("id"),
rs.getString("name"),
// ...
objectMapper.readValue(rs.getString("tags"), new TypeReference<List<String>>() {}), // JSON → List
// ...
);
}
}// data/cosmos/CosmosProductRepository.java
@Repository
@Profile("cosmos")
public class CosmosProductRepository implements ProductRepository {
private final CosmosContainer container;
public CosmosProductRepository(CosmosClient client,
@Value("${cosmos.database:aimarket}") String dbName) {
this.container = client.getDatabase(dbName).getContainer("products");
}
@Override
public PaginatedResult<Product> getAll(int page, int pageSize,
String category, Double minPrice, Double maxPrice) {
var query = new StringBuilder("SELECT * FROM c WHERE c.status = @status");
var params = new ArrayList<SqlParameter>();
params.add(new SqlParameter("@status", "active"));
if (category != null) {
query.append(" AND c.category = @cat");
params.add(new SqlParameter("@cat", category));
}
// ... build query
var options = new CosmosQueryRequestOptions();
var items = container.queryItems(
new SqlQuerySpec(query.toString(), params), options, Product.class);
var all = items.stream().collect(Collectors.toList());
int start = (page - 1) * pageSize;
return new PaginatedResult<>(
all.subList(start, Math.min(start + pageSize, all.size())), all.size());
}
}SQLite DataSource Config
// config/SqliteConfig.java
@Configuration
@Profile("sqlite")
public class SqliteConfig {
@Bean
public DataSource dataSource() {
var ds = new SQLiteDataSource();
ds.setUrl("jdbc:sqlite:app.db");
return ds;
}
@Bean
public CommandLineRunner initDb(DataSource dataSource) {
return args -> {
var jdbc = new JdbcTemplate(dataSource);
jdbc.execute("PRAGMA journal_mode=WAL");
jdbc.execute("PRAGMA foreign_keys=ON");
// Create tables, seed if empty...
};
}
}Controller Routes
// controller/ProductController.java — depends only on ProductRepository interface
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository repo;
public ProductController(ProductRepository repo) { // Spring injects the active profile's impl
this.repo = repo;
}
@GetMapping
public ResponseEntity<?> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String category) {
var result = repo.getAll(page, pageSize, category, null, null);
return ResponseEntity.ok(Map.of(
"data", result.data(), "page", page, "pageSize", pageSize,
"totalCount", result.totalCount(),
"totalPages", (int) Math.ceil((double) result.totalCount() / pageSize)
));
}
}Key: Spring's @Profile + DI eliminates the manual factory. Set DATA_PROVIDER=postgres or DATA_PROVIDER=cosmos as the active profile and Spring wires the matching implementations automatically. Add @Profile("postgres") to a PostgresProductRepository that uses JdbcTemplate with a PostgreSQL DataSource — same JdbcTemplate API as SQLite, different driver and SQL dialect.
SQLite Patterns (All Languages)
SQLite doesn't support arrays or nested objects. These patterns apply regardless of language:
| Data Type | SQLite Storage | Read Pattern |
string[] (tags) | JSON string column: '["tag1","tag2"]' | Parse with JSON.parse() / json.loads() / JsonSerializer.Deserialize / ObjectMapper.readValue() |
| Nested object (address) | JSON string column: '{"street":"..."}' | Parse on read |
| One-to-many (order items) | Junction table: order_items(orderId, productId, quantity, price) | JOIN on read |
| UUID primary keys | TEXT column | Generate with uuid / uuid4() / Guid.NewGuid() / UUID.randomUUID() |
Pragmas to always set:
PRAGMA journal_mode = WAL; -- Better concurrent read performance
PRAGMA foreign_keys = ON; -- Enforce referential integrityCosmos DB Patterns (All Languages)
| Data Type | Cosmos DB Storage | Notes |
string[] (tags) | Native JSON array | No serialization needed |
| Nested object (address) | Native JSON object | No serialization needed |
| One-to-many (order items) | Embedded array in document | No junction table needed |
| Queries | SQL-like with @param | Use parameterized queries for safety |
Partition key strategy: Choose a field that distributes data evenly. Common choices: /id (default), /category (products), /userId (orders).
PostgreSQL Patterns (All Languages)
PostgreSQL supports native arrays and JSONB, so it handles complex types better than SQLite but uses SQL instead of Cosmos DB's document model.
| Data Type | PostgreSQL Storage | Notes |
string[] (tags) | TEXT[] (native array) | Use = ANY($1) for filtering, no serialization needed |
| Nested object (address) | JSONB column | Query with -> / ->> operators |
| One-to-many (order items) | Junction table (same as SQLite) or JSONB array | Junction table is cleaner for queries |
| UUID primary keys | UUID type or TEXT | Use gen_random_uuid() for server-side generation |
| Parameterized queries | $1, $2, $3 (Node pg) or %s (Python psycopg2) or @param (.NET Npgsql) | Never concatenate user input |
Azure PostgreSQL requirements:
# Connection string must include sslmode
POSTGRES_CONNECTION_STRING=postgresql://user:pass@host.postgres.database.azure.com:5432/aimarket?sslmode=require
# For Azure Flexible Server, also set
POSTGRES_SSL=trueLibraries by language:
| Language | Library | Notes |
| Node.js | pg (node-postgres) | Use Pool for connection pooling |
| Python | psycopg2 or asyncpg | psycopg2-binary for easy install, asyncpg for async FastAPI |
| .NET | Npgsql + Npgsql.EntityFrameworkCore.PostgreSQL | Or raw NpgsqlConnection |
| Java | Spring JdbcTemplate + postgresql driver | Same JdbcTemplate API as SQLite, different DataSource |
Anti-Patterns
| Don't | Do Instead |
| Import database clients in route files | Import only interfaces/protocols |
| Put SQL/query strings in routes | Encapsulate in the implementation |
| Hardcode connection strings | Use environment variables |
| Create one repository for all entities | One interface per entity |
Skip async/await because SQLite is sync | Always use async — it's free for sync implementations |
| Test against a real database | Create an in-memory implementation for unit tests |
Checklist
- Repository interfaces defined per entity (typed, no database-specific imports)
- Factory/DI reads
DATA_PROVIDERand returns the correct implementation - Routes depend only on interfaces, never on implementation files
- SQLite implementation handles JSON serialization for arrays and objects
- PostgreSQL implementation uses native
TEXT[]for arrays andJSONBfor objects - Cosmos DB implementation uses native JSON types (no serialization)
DATA_PROVIDER=sqliteworks locallyDATA_PROVIDER=postgresworks with Azure PostgreSQL (SSL enabled)DATA_PROVIDER=cosmosworks with Azure Cosmos DB.env.exampledocuments all providers and their required variables- An in-memory implementation exists for unit tests
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

