Node.js & Electron Integration
This guide shows how to connect to every WeftKit engine from Node.js, TypeScript, Express, NestJS, and Electron using standard database drivers. WeftKit Standalone speaks the same wire protocols as well-known databases, so any driver that works with PostgreSQL, MongoDB, Redis, Neo4j, DynamoDB, or gRPC will work with WeftKit.
All examples assume weftkit-standalone is running locally. Adjust the host and port to match your deployment.
Prerequisites
Install the packages you need for the engines your application uses:
bash# WeftKitRel — PostgreSQL protocol npm install pg npm install postgres # modern alternative # WeftKitDoc — MongoDB Wire protocol npm install mongodb # WeftKitMem — Redis RESP3 npm install ioredis # WeftKitGraph — Bolt (Neo4j) npm install neo4j-driver # WeftKitVec — gRPC npm install @grpc/grpc-js @grpc/proto-loader # WeftKitKV — DynamoDB REST npm install @aws-sdk/client-dynamodb # TypeScript types (optional but recommended) npm install --save-dev @types/pg
Environment Variables
Store all connection details in a .env file and load them with dotenv or Node's built-in --env-file flag:
bash# .env # WeftKitRel WEFTKIT_REL_URL=postgresql://myuser:mypassword@localhost:5432/mydb # WeftKitDoc WEFTKIT_DOC_URL=mongodb://myuser:mypassword@localhost:27017/mydb # WeftKitMem WEFTKIT_MEM_HOST=localhost WEFTKIT_MEM_PORT=6379 WEFTKIT_MEM_PASSWORD=mypassword # WeftKitGraph WEFTKIT_GRAPH_URI=bolt://localhost:7687 WEFTKIT_GRAPH_USER=neo4j WEFTKIT_GRAPH_PASSWORD=mypassword # WeftKitVec WEFTKIT_VEC_TARGET=localhost:50051 # WeftKitKV WEFTKIT_KV_ENDPOINT=http://localhost:8000 WEFTKIT_KV_REGION=us-east-1 WEFTKIT_KV_ACCESS_KEY=any-value WEFTKIT_KV_SECRET_KEY=any-value
Load them at the top of your entry point:
typescriptimport "dotenv/config"; // npm install dotenv
1. WeftKitRel (PostgreSQL) via pg
pg (node-postgres) is the most widely used PostgreSQL driver for Node.js. Create a connection pool once and reuse it throughout your application.
Connection Pool Setup
typescriptimport { Pool, PoolClient } from "pg"; const pool = new Pool({ connectionString: process.env.WEFTKIT_REL_URL, max: 20, // maximum number of connections in the pool idleTimeoutMillis: 30_000, connectionTimeoutMillis: 2_000, }); // Verify connectivity on startup pool.on("error", (err) => { console.error("Unexpected pool error:", err); }); export default pool;
Basic SELECT Query
typescriptasync function getUsers(): Promise<{ id: number; name: string; email: string }[]> { const result = await pool.query( "SELECT id, name, email FROM users ORDER BY name" ); return result.rows; }
Parameterized INSERT
Use $1, $2, ... placeholders to safely interpolate values. The driver handles escaping.
typescriptasync function createUser(name: string, email: string): Promise<number> { const result = await pool.query( "INSERT INTO users (name, email, created_at) VALUES ($1, $2, NOW()) RETURNING id", [name, email] ); return result.rows[0].id; }
Transactions
Check out a dedicated client from the pool to run multiple statements in a single transaction:
typescriptasync function transferBalance( fromId: number, toId: number, amount: number ): Promise<void> { const client: PoolClient = await pool.connect(); try { await client.query("BEGIN"); await client.query( "UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, fromId] ); await client.query( "UPDATE accounts SET balance = balance + $1 WHERE id = $2", [amount, toId] ); await client.query("COMMIT"); } catch (err) { await client.query("ROLLBACK"); throw err; } finally { client.release(); } }
Error Handling
typescriptimport { DatabaseError } from "pg"; async function safeInsert(name: string, email: string): Promise<void> { try { await pool.query( "INSERT INTO users (name, email) VALUES ($1, $2)", [name, email] ); } catch (err) { if (err instanceof DatabaseError) { if (err.code === "23505") { throw new Error(`User with email ${email} already exists`); } } throw err; } }
Graceful Shutdown
typescriptprocess.on("SIGTERM", async () => { await pool.end(); process.exit(0); });
2. WeftKitRel via postgres (Postgres.js)
Postgres.js offers a modern API using tagged template literals, which prevents SQL injection by design.
Connection Setup
typescriptimport postgres from "postgres"; const sql = postgres(process.env.WEFTKIT_REL_URL!, { max: 10, idle_timeout: 20, connect_timeout: 10, }); export default sql;
Queries with Tagged Template Literals
Values embedded in the template are automatically parameterized — you cannot accidentally construct a SQL injection:
typescriptasync function getUsersByRole(role: string) { const users = await sql` SELECT id, name, email, created_at FROM users WHERE role = ${role} ORDER BY name `; return users; } async function createProduct(name: string, price: number, stock: number) { const [product] = await sql` INSERT INTO products (name, price, stock) VALUES (${name}, ${price}, ${stock}) RETURNING * `; return product; }
Transactions
typescriptasync function placeOrder(userId: number, productId: number, qty: number) { return await sql.begin(async (tx) => { const [product] = await tx` SELECT id, price, stock FROM products WHERE id = ${productId} FOR UPDATE `; if (product.stock < qty) { throw new Error("Insufficient stock"); } await tx` UPDATE products SET stock = stock - ${qty} WHERE id = ${productId} `; const [order] = await tx` INSERT INTO orders (user_id, product_id, quantity, total) VALUES (${userId}, ${productId}, ${qty}, ${product.price * qty}) RETURNING * `; return order; }); }
3. WeftKitDoc (MongoDB) via mongodb
WeftKit Standalone speaks the MongoDB Wire Protocol. The official mongodb Node.js driver connects without any modification.
MongoClient Connection
typescriptimport { MongoClient, Db } from "mongodb"; const client = new MongoClient(process.env.WEFTKIT_DOC_URL!, { maxPoolSize: 20, serverSelectionTimeoutMS: 5_000, }); let db: Db; export async function connectDoc(): Promise<Db> { if (!db) { await client.connect(); db = client.db("mydb"); } return db; }
Insert One / Insert Many
typescriptinterface Product { name: string; category: string; price: number; tags: string[]; } const db = await connectDoc(); const products = db.collection<Product>("products"); // Single document const result = await products.insertOne({ name: "Wireless Headphones", category: "electronics", price: 149.99, tags: ["audio", "wireless"], }); console.log("Inserted:", result.insertedId); // Multiple documents const { insertedCount } = await products.insertMany([ { name: "USB-C Hub", category: "electronics", price: 39.99, tags: ["usb"] }, { name: "Desk Lamp", category: "office", price: 24.99, tags: ["lighting"] }, ]); console.log(`Inserted ${insertedCount} documents`);
Find with Filter Operators
typescript// Price range query const affordable = await products .find({ price: { $lt: 50 } }) .sort({ price: 1 }) .toArray(); // Multiple categories const multiCat = await products .find({ category: { $in: ["electronics", "office"] } }) .toArray(); // Combined conditions const specific = await products .find({ $and: [ { price: { $gt: 20, $lt: 100 } }, { tags: { $in: ["wireless", "usb"] } }, ], }) .toArray();
UpdateOne with $set and $inc
typescript// Set new values await products.updateOne( { name: "Wireless Headphones" }, { $set: { price: 129.99, "meta.lastUpdated": new Date() } } ); // Increment a counter await products.updateOne( { name: "USB-C Hub" }, { $inc: { viewCount: 1 } } ); // Upsert (create if not found) await products.updateOne( { name: "Mechanical Keyboard" }, { $set: { category: "electronics", price: 89.99, tags: ["keyboard"] } }, { upsert: true } );
Aggregation Pipeline
typescriptconst categorySummary = await products .aggregate([ { $match: { price: { $gt: 0 } } }, { $group: { _id: "$category", count: { $sum: 1 }, avgPrice: { $avg: "$price" }, minPrice: { $min: "$price" }, maxPrice: { $max: "$price" }, }, }, { $sort: { avgPrice: -1 } }, ]) .toArray();
Index Creation
typescript// Single-field index await products.createIndex({ category: 1 }); // Compound index await products.createIndex({ category: 1, price: -1 }); // Unique index await products.createIndex({ sku: 1 }, { unique: true }); // Text search index await products.createIndex({ name: "text", tags: "text" });
4. WeftKitMem (Redis) via ioredis
WeftKit Standalone speaks Redis RESP3. ioredis connects without modification and supports all standard Redis commands.
Connection Setup
typescriptimport Redis from "ioredis"; const redis = new Redis({ host: process.env.WEFTKIT_MEM_HOST ?? "localhost", port: Number(process.env.WEFTKIT_MEM_PORT ?? 6379), password: process.env.WEFTKIT_MEM_PASSWORD, maxRetriesPerRequest: 3, enableReadyCheck: true, }); redis.on("error", (err) => console.error("Redis error:", err)); redis.on("connect", () => console.log("Redis connected")); export default redis;
SET / GET with TTL
typescript// Set a key with a 10-minute TTL (seconds) await redis.set("session:abc123", JSON.stringify({ userId: 42 }), "EX", 600); // Get and parse const raw = await redis.get("session:abc123"); const session = raw ? JSON.parse(raw) : null; // Set only if key does not exist (NX flag) const ok = await redis.set("lock:resource", "1", "EX", 30, "NX"); if (!ok) { throw new Error("Could not acquire lock — already held"); } // Delete await redis.del("session:abc123");
Hash Maps (HSET / HGETALL)
typescript// Store a user profile as a hash await redis.hset("user:1001", { name: "Alice", email: "alice@example.com", role: "admin", loginCount: "0", }); // Get all fields const profile = await redis.hgetall("user:1001"); // { name: 'Alice', email: 'alice@example.com', ... } // Get a single field const name = await redis.hget("user:1001", "name"); // Increment a numeric field await redis.hincrby("user:1001", "loginCount", 1);
Lists (LPUSH / LRANGE)
typescript// Prepend items to a list (newest first) await redis.lpush("notifications:user:1001", JSON.stringify({ msg: "New comment", ts: Date.now() })); await redis.lpush("notifications:user:1001", JSON.stringify({ msg: "New follower", ts: Date.now() })); // Read the first 20 notifications (0-indexed) const rawItems = await redis.lrange("notifications:user:1001", 0, 19); const notifications = rawItems.map((r) => JSON.parse(r)); // Trim to last 100 entries to cap memory usage await redis.ltrim("notifications:user:1001", 0, 99);
Sorted Sets (ZADD / ZRANGE)
typescript// Leaderboard — score is the value used for ranking await redis.zadd("leaderboard:week", 9800, "alice"); await redis.zadd("leaderboard:week", 8500, "bob"); await redis.zadd("leaderboard:week", 7200, "carol"); // Top 3 (highest scores first) const topThree = await redis.zrange("leaderboard:week", 0, 2, "REV", "WITHSCORES"); // ["alice", "9800", "bob", "8500", "carol", "7200"] // Rank of a specific player (0 = highest) const rank = await redis.zrevrank("leaderboard:week", "bob"); // Increment score await redis.zincrby("leaderboard:week", 200, "carol");
Pub/Sub
Use two separate ioredis instances — one for publishing, one for subscribing. A subscribed connection cannot issue regular commands.
typescriptimport Redis from "ioredis"; const publisher = new Redis({ host: "localhost", port: 6379 }); const subscriber = new Redis({ host: "localhost", port: 6379 }); // Subscribe to a channel await subscriber.subscribe("events:orders"); subscriber.on("message", (channel, message) => { const event = JSON.parse(message); console.log(`[${channel}]`, event); }); // Publish from anywhere in your application await publisher.publish("events:orders", JSON.stringify({ orderId: 42, status: "shipped" }));
Redis Streams (XADD / XREAD)
Streams provide a persistent, ordered log — useful for event sourcing and task queues:
typescript// Append an event to the stream (auto-generate ID with "*") const id = await redis.xadd( "stream:orders", "*", "orderId", "42", "userId", "1001", "total", "99.99", "status", "pending" ); console.log("Stream entry ID:", id); // Read up to 10 entries from the beginning const entries = await redis.xread("COUNT", 10, "STREAMS", "stream:orders", "0-0"); if (entries) { for (const [_stream, messages] of entries) { for (const [entryId, fields] of messages) { console.log(entryId, fields); } } } // Consumer group for distributed processing await redis.xgroup("CREATE", "stream:orders", "order-processors", "$", "MKSTREAM"); const groupEntries = await redis.xreadgroup( "GROUP", "order-processors", "worker-1", "COUNT", 5, "STREAMS", "stream:orders", ">" );
5. WeftKitGraph (Neo4j Bolt) via neo4j-driver
WeftKit Standalone speaks the Bolt protocol. The official neo4j-driver connects directly.
Driver Initialization
typescriptimport neo4j from "neo4j-driver"; const driver = neo4j.driver( process.env.WEFTKIT_GRAPH_URI ?? "bolt://localhost:7687", neo4j.auth.basic( process.env.WEFTKIT_GRAPH_USER ?? "neo4j", process.env.WEFTKIT_GRAPH_PASSWORD ?? "password" ), { maxConnectionPoolSize: 50 } ); // Verify connectivity await driver.verifyConnectivity(); console.log("Graph connected"); export default driver;
Run a Cypher Query
Open a session, run a query, consume results, then close:
typescriptasync function getPersonByName(name: string) { const session = driver.session({ database: "neo4j" }); try { const result = await session.run( "MATCH (p:Person { name: $name }) RETURN p", { name } ); return result.records.map((r) => r.get("p").properties); } finally { await session.close(); } }
Read Transactions
Wrap reads in executeRead so the driver can retry on transient failures and route to read replicas in a cluster:
typescriptasync function getFriends(personId: string): Promise<string[]> { const session = driver.session(); try { return await session.executeRead(async (tx) => { const result = await tx.run( `MATCH (p:Person { id: $personId })-[:FRIEND]->(f:Person) RETURN f.name AS name ORDER BY name`, { personId } ); return result.records.map((r) => r.get("name") as string); }); } finally { await session.close(); } }
Write Transactions
typescriptasync function createFriendship(personAId: string, personBId: string): Promise<void> { const session = driver.session(); try { await session.executeWrite(async (tx) => { await tx.run( `MATCH (a:Person { id: $personAId }), (b:Person { id: $personBId }) MERGE (a)-[:FRIEND]->(b) MERGE (b)-[:FRIEND]->(a)`, { personAId, personBId } ); }); } finally { await session.close(); } }
Graph Traversal Example
Find all people within 3 hops of a starting node:
typescriptasync function shortestPath(fromId: string, toId: string) { const session = driver.session(); try { return await session.executeRead(async (tx) => { const result = await tx.run( `MATCH path = shortestPath( (a:Person { id: $fromId })-[*..6]-(b:Person { id: $toId }) ) RETURN [node IN nodes(path) | node.name] AS names, length(path) AS hops`, { fromId, toId } ); return result.records.map((r) => ({ names: r.get("names") as string[], hops: (r.get("hops") as unknown as { toNumber(): number }).toNumber(), })); }); } finally { await session.close(); } }
Graceful Shutdown
typescriptprocess.on("SIGTERM", async () => { await driver.close(); process.exit(0); });
6. WeftKitVec (gRPC) via @grpc/grpc-js
WeftKit exposes its vector engine over gRPC. Load the .proto file distributed with WeftKit Standalone and create a typed client.
Proto File (provided with WeftKit Standalone)
protobuf// weftkit_vec.proto syntax = "proto3"; package weftkit.vec; service VectorService { rpc Upsert (UpsertRequest) returns (UpsertResponse); rpc Search (SearchRequest) returns (SearchResponse); rpc Delete (DeleteRequest) returns (DeleteResponse); } message Vector { string id = 1; repeated float embedding = 2; map<string, string> metadata = 3; } message UpsertRequest { repeated Vector vectors = 1; string namespace = 2; } message UpsertResponse { int32 upserted_count = 1; } message SearchRequest { repeated float query_vector = 1; int32 top_k = 2; string namespace = 3; string filter = 4; } message SearchResult { string id = 1; float score = 2; map<string, string> metadata = 3; } message SearchResponse { repeated SearchResult results = 1; } message DeleteRequest { repeated string ids = 1; string namespace = 2; } message DeleteResponse { int32 deleted_count = 1; }
Client Setup
typescriptimport * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; const PROTO_PATH = path.join(__dirname, "protos", "weftkit_vec.proto"); const packageDef = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, }); const protoDesc = grpc.loadPackageDefinition(packageDef) as any; const VecService = protoDesc.weftkit.vec.VectorService; const vecClient = new VecService( process.env.WEFTKIT_VEC_TARGET ?? "localhost:50051", grpc.credentials.createInsecure() // use createSsl() for TLS ); export default vecClient;
Upsert Vectors
typescriptimport { promisify } from "util"; const upsert = promisify(vecClient.Upsert.bind(vecClient)); async function upsertEmbeddings( vectors: Array<{ id: string; embedding: number[]; metadata: Record<string, string> }> ) { const response = await upsert({ namespace: "products", vectors: vectors.map(({ id, embedding, metadata }) => ({ id, embedding, metadata, })), }); console.log(`Upserted ${response.upserted_count} vectors`); }
Similarity Search
typescriptconst search = promisify(vecClient.Search.bind(vecClient)); async function findSimilar(queryEmbedding: number[], topK = 10) { const response = await search({ query_vector: queryEmbedding, top_k: topK, namespace: "products", filter: '{"category": "electronics"}', // optional metadata filter }); return response.results.map( (r: { id: string; score: number; metadata: Record<string, string> }) => ({ id: r.id, score: r.score, metadata: r.metadata, }) ); }
7. WeftKitKV (DynamoDB REST) via @aws-sdk/client-dynamodb
Point the AWS SDK at WeftKit Standalone's DynamoDB-compatible REST endpoint. Use any non-empty string for the region, access key, and secret key — WeftKit validates using its own credentials system.
Client Setup
typescriptimport { DynamoDBClient, PutItemCommand, GetItemCommand, DeleteItemCommand, QueryCommand, } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; const ddb = new DynamoDBClient({ endpoint: process.env.WEFTKIT_KV_ENDPOINT ?? "http://localhost:8000", region: process.env.WEFTKIT_KV_REGION ?? "us-east-1", credentials: { accessKeyId: process.env.WEFTKIT_KV_ACCESS_KEY ?? "weftkit", secretAccessKey: process.env.WEFTKIT_KV_SECRET_KEY ?? "weftkit", }, }); export default ddb;
PutItem
typescriptawait ddb.send(new PutItemCommand({ TableName: "Sessions", Item: marshall({ sessionId: "sess_abc123", userId: 42, expiresAt: Math.floor(Date.now() / 1000) + 3600, data: { theme: "dark", locale: "en-US" }, }), }));
GetItem
typescriptconst response = await ddb.send(new GetItemCommand({ TableName: "Sessions", Key: marshall({ sessionId: "sess_abc123" }), })); const session = response.Item ? unmarshall(response.Item) : null;
DeleteItem
typescriptawait ddb.send(new DeleteItemCommand({ TableName: "Sessions", Key: marshall({ sessionId: "sess_abc123" }), }));
QueryCommand
typescriptimport { AttributeValue } from "@aws-sdk/client-dynamodb"; const result = await ddb.send(new QueryCommand({ TableName: "Orders", KeyConditionExpression: "userId = :uid AND createdAt > :since", ExpressionAttributeValues: marshall({ ":uid": 1001, ":since": "2025-01-01T00:00:00Z", }) as Record<string, AttributeValue>, ScanIndexForward: false, // descending order Limit: 20, })); const orders = (result.Items ?? []).map(unmarshall);
8. Express.js Integration Pattern
Centralized Database Module (src/db.ts)
Create one module that initializes all connections and exports them. Import this module in your application entry point before starting the HTTP server.
typescript// src/db.ts import { Pool } from "pg"; import { MongoClient, Db } from "mongodb"; import Redis from "ioredis"; export const relPool = new Pool({ connectionString: process.env.WEFTKIT_REL_URL, max: 20, }); let _docDb: Db; const mongoClient = new MongoClient(process.env.WEFTKIT_DOC_URL!); export async function getDocDb(): Promise<Db> { if (!_docDb) { await mongoClient.connect(); _docDb = mongoClient.db(); } return _docDb; } export const memClient = new Redis({ host: process.env.WEFTKIT_MEM_HOST, port: Number(process.env.WEFTKIT_MEM_PORT ?? 6379), password: process.env.WEFTKIT_MEM_PASSWORD, }); export async function closeAll(): Promise<void> { await relPool.end(); await mongoClient.close(); memClient.disconnect(); }
Middleware to Attach Pool to Requests (src/middleware/db.ts)
typescript// src/middleware/db.ts import { Request, Response, NextFunction } from "express"; import { relPool } from "../db"; declare global { namespace Express { interface Request { relPool: typeof relPool; } } } export function attachDb(req: Request, _res: Response, next: NextFunction) { req.relPool = relPool; next(); }
Application Entry Point (src/app.ts)
typescript// src/app.ts import "dotenv/config"; import express from "express"; import { attachDb } from "./middleware/db"; import { closeAll } from "./db"; import usersRouter from "./routes/users"; const app = express(); app.use(express.json()); app.use(attachDb); app.use("/users", usersRouter); const server = app.listen(3000, () => console.log("Server listening on port 3000") ); process.on("SIGTERM", async () => { server.close(async () => { await closeAll(); process.exit(0); }); });
Example Route (src/routes/users.ts)
typescript// src/routes/users.ts import { Router, Request, Response } from "express"; const router = Router(); router.get("/", async (req: Request, res: Response) => { try { const { rows } = await req.relPool.query( "SELECT id, name, email FROM users ORDER BY name LIMIT 100" ); res.json(rows); } catch (err) { console.error(err); res.status(500).json({ error: "Database error" }); } }); router.post("/", async (req: Request, res: Response) => { const { name, email } = req.body; if (!name || !email) { return res.status(400).json({ error: "name and email required" }); } try { const { rows } = await req.relPool.query( "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *", [name, email] ); res.status(201).json(rows[0]); } catch (err) { console.error(err); res.status(500).json({ error: "Database error" }); } }); export default router;
9. NestJS Integration Pattern
Database Module (src/database/database.module.ts)
typescript// src/database/database.module.ts import { Module, Global, OnApplicationShutdown } from "@nestjs/common"; import { Pool } from "pg"; const REL_POOL_PROVIDER = { provide: "REL_POOL", useFactory: () => new Pool({ connectionString: process.env.WEFTKIT_REL_URL, max: 20 }), }; @Global() @Module({ providers: [REL_POOL_PROVIDER], exports: ["REL_POOL"], }) export class DatabaseModule implements OnApplicationShutdown { constructor(@Inject("REL_POOL") private readonly pool: Pool) {} async onApplicationShutdown() { await this.pool.end(); } }
Import DatabaseModule once in AppModule and it becomes available to all feature modules.
Database Service (src/database/database.service.ts)
typescript// src/database/database.service.ts import { Injectable, Inject } from "@nestjs/common"; import { Pool, QueryResult } from "pg"; @Injectable() export class DatabaseService { constructor(@Inject("REL_POOL") private readonly pool: Pool) {} async query<T>(text: string, values?: unknown[]): Promise<QueryResult<T>> { return this.pool.query<T>(text, values); } async transaction<T>( fn: (client: import("pg").PoolClient) => Promise<T> ): Promise<T> { const client = await this.pool.connect(); try { await client.query("BEGIN"); const result = await fn(client); await client.query("COMMIT"); return result; } catch (err) { await client.query("ROLLBACK"); throw err; } finally { client.release(); } } }
Example Controller (src/users/users.controller.ts)
typescript// src/users/users.controller.ts import { Controller, Get, Post, Body, Param, ParseIntPipe } from "@nestjs/common"; import { DatabaseService } from "../database/database.service"; interface User { id: number; name: string; email: string; } @Controller("users") export class UsersController { constructor(private readonly db: DatabaseService) {} @Get() async findAll(): Promise<User[]> { const result = await this.db.query<User>( "SELECT id, name, email FROM users ORDER BY name" ); return result.rows; } @Get(":id") async findOne(@Param("id", ParseIntPipe) id: number): Promise<User | null> { const result = await this.db.query<User>( "SELECT id, name, email FROM users WHERE id = $1", [id] ); return result.rows[0] ?? null; } @Post() async create(@Body() body: { name: string; email: string }): Promise<User> { const result = await this.db.query<User>( "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *", [body.name, body.email] ); return result.rows[0]; } }
10. Electron Integration Pattern
In Electron, all database connections must live in the main process. Renderer processes are sandboxed browser contexts with no direct TCP access. Expose database operations to the renderer via IPC handlers.
Architecture Overview
Renderer Process
│ ipcRenderer.invoke("db:users:list", { limit: 50 })
▼
IPC Bridge (preload.ts)
│
▼
Main Process (main.ts)
│ pool.query("SELECT …")
▼
WeftKit Standalone (TCP)
Main Process (src/main/main.ts)
typescript// src/main/main.ts import { app, BrowserWindow, ipcMain } from "electron"; import { Pool } from "pg"; const pool = new Pool({ connectionString: process.env.WEFTKIT_REL_URL ?? "postgresql://admin:password@localhost:5432/mydb", max: 5, }); // Register IPC handlers before creating any window ipcMain.handle("db:users:list", async (_event, { limit = 100 } = {}) => { const { rows } = await pool.query( "SELECT id, name, email FROM users ORDER BY name LIMIT $1", [limit] ); return rows; }); ipcMain.handle("db:users:create", async (_event, { name, email }: { name: string; email: string }) => { const { rows } = await pool.query( "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *", [name, email] ); return rows[0]; }); app.whenReady().then(() => { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: __dirname + "/preload.js", contextIsolation: true, // required — do not disable nodeIntegration: false, // required — do not enable }, }); win.loadFile("dist/renderer/index.html"); }); app.on("quit", async () => { await pool.end(); });
Preload Script (src/main/preload.ts)
typescript// src/main/preload.ts import { contextBridge, ipcRenderer } from "electron"; contextBridge.exposeInMainWorld("weftkit", { users: { list: (options?: { limit?: number }) => ipcRenderer.invoke("db:users:list", options), create: (data: { name: string; email: string }) => ipcRenderer.invoke("db:users:create", data), }, });
Renderer Process (src/renderer/app.ts)
The renderer only calls the exposed bridge — it has no direct database access:
typescript// src/renderer/app.ts declare global { interface Window { weftkit: { users: { list: (opts?: { limit?: number }) => Promise<{ id: number; name: string; email: string }[]>; create: (data: { name: string; email: string }) => Promise<{ id: number; name: string; email: string }>; }; }; } } // Load user list const users = await window.weftkit.users.list({ limit: 50 }); console.log(users); // Create a user const newUser = await window.weftkit.users.create({ name: "Bob", email: "bob@example.com" }); console.log(newUser);
Note: Never install
pg,ioredis, or other database drivers as renderer-side dependencies. All database packages belong in the main process. SetnodeIntegration: falseandcontextIsolation: trueinBrowserWindow.webPreferences— these are required security settings.
Connection String Quick Reference
| Engine | Format |
|---|---|
| WeftKitRel | postgresql://user:pass@host:5432/dbname |
| WeftKitDoc | mongodb://user:pass@host:27017/dbname |
| WeftKitMem | redis://:pass@host:6379/0 |
| WeftKitGraph | bolt://host:7687 |
| WeftKitVec | host:50051 (gRPC target) |
| WeftKitKV | http://host:8000 (endpoint override) |
All ports are configurable in weftkit.toml. See the Security guide for TLS and JWT configuration.
On this page