Skip to content
WeftKitBeta

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:

typescript
import "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

typescript
import { 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

typescript
async 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.

typescript
async 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:

typescript
async 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

typescript
import { 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

typescript
process.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

typescript
import 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:

typescript
async 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

typescript
async 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

typescript
import { 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

typescript
interface 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

typescript
const 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

typescript
import 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.

typescript
import 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

typescript
import 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:

typescript
async 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:

typescript
async 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

typescript
async 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:

typescript
async 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

typescript
process.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

typescript
import * 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

typescript
import { 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

typescript
const 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

typescript
import {
  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

typescript
await 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

typescript
const response = await ddb.send(new GetItemCommand({
  TableName: "Sessions",
  Key: marshall({ sessionId: "sess_abc123" }),
}));

const session = response.Item ? unmarshall(response.Item) : null;

DeleteItem

typescript
await ddb.send(new DeleteItemCommand({
  TableName: "Sessions",
  Key: marshall({ sessionId: "sess_abc123" }),
}));

QueryCommand

typescript
import { 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. Set nodeIntegration: false and contextIsolation: true in BrowserWindow.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.