npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@speles7172/sql-bridge

v0.3.0

Published

Lambda handlers that reach databases a caller cannot, for @speles7172/sql-client.

Readme

@speles7172/sql-bridge

Lambda handlers that run queries against databases a caller cannot reach — another VPC, another region, or an engine whose driver has no business being in every consumer's bundle.

The other half of @speles7172/sql-client: register a database with engine: 'remote-pg' or engine: 'neo4j' and a bridgeFunctionName, and the client invokes one of these.

your Lambda ──invoke──▶ sql-bridge ──▶ database in another VPC / region
 (sql-client)            (this)

Why a separate function at all

Three reasons, in order of how much they matter:

  1. Reachability. A database in another VPC or region is not addressable from your process. A small function that is addressable, placed where the database lives, is the cheapest thing that fixes that.
  2. Driver weight. Neo4j goes through the bridge specifically so that sql-client never depends on neo4j-driver. A service that only queries PostgreSQL should not carry a graph driver.
  3. Blast radius. The bridge holds credentials for databases nothing else can reach. Keeping it separate means one narrow IAM role reads those secrets, rather than every service that might want the data.

Two functions, not one

postgres and neo4j deploy separately. They need different drivers, different ports and different secrets, and one function switching on engine would put the graph driver into the artifact of every PostgreSQL query.

| Engine | Handler | Port default | |---|---|---| | postgres | dist/postgres-handler.handler | 5432 | | neo4j | dist/neo4j-handler.handler | 7687 |


Deploying it from your own repository

This repository ships the handler and the Terraform module; it does not run bridges for you. A bridge belongs to the environment whose databases it reaches — the subnets, security groups and secret ARNs it needs all live in that account — so you instantiate it where those things are.

Three steps.

1. Bundle the handler

npm install @speles7172/sql-bridge
npx esbuild node_modules/@speles7172/sql-bridge/dist/postgres-handler.js \
  --bundle --platform=node --target=node22 --format=esm \
  --external:@aws-sdk/* \
  --outfile=build/bridge/dist/postgres-handler.js

Notes on that command:

  • --external:@aws-sdk/* — the SDK is already in the Lambda runtime; bundling it roughly triples the artifact for no benefit.
  • The output path must keep dist/postgres-handler.js, because the module sets the Lambda handler to dist/postgres-handler.handler.
  • For Neo4j, swap both filenames and do not externalise neo4j-driver — it is not in the runtime.

2. Provision it

data "archive_file" "bridge" {
  type        = "zip"
  source_dir  = "${path.module}/build/bridge"
  output_path = "${path.module}/build/bridge.zip"
}

module "reporting_bridge" {
  source = "git::https://github.com/speles7172/utils.git//infra/modules/sql-bridge?ref=v1.0.0"

  name_prefix  = "myapp-staging"
  engine       = "postgres"
  package_path = data.archive_file.bridge.output_path

  # Exactly the secrets this bridge serves — see below.
  allowed_secret_arns = [aws_db_instance.reporting.master_user_secret[0].secret_arn]

  # Where the database actually is.
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [aws_security_group.bridge.id]
}

Pin ?ref= to a tag. Tracking main means an unrelated merge here can change your infrastructure.

Your caller also needs lambda:InvokeFunction on module.reporting_bridge.function_arn.

3. Register it with the client

registerDatabase({
  name: 'reporting',
  engine: 'remote-pg',
  version: '16',
  region: 'us-west-2',
  secretArn: 'arn:aws:secretsmanager:us-west-2:…:secret:reporting',
  bridgeFunctionName: 'myapp-staging-sql-bridge-postgres',
});

Verify the database's certificate

By default the bridge encrypts its connection but does not authenticate the server — the same default as the client, and for the same reason: RDS presents a certificate signed by a private Amazon CA that is in no default trust store, so verifying out of the box would break every connection on first use.

A bridge usually sits closer to untrusted network than the caller does, so this is worth changing. Give the module a CA and it verifies:

module "reporting_bridge" {
  # …
  ssl_ca_pem = file("${path.module}/rds-global-bundle.pem")
}

from https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem.

| Setting | Behaviour | |---|---| | default | encrypted, server not authenticated | | ssl_ca_pem set | encrypted and verified against that CA | | ssl_reject_unauthorized = false | forces verification off even with a CA | | BRIDGE_SSL=false in environment | no TLS — loopback testing only |

Neo4j uses the neo4j+s scheme, which verifies, and does work out of the box against Aura or any publicly-signed host. Drop to neo4j_scheme = "neo4j+ssc" only for a self-signed deployment.

Trust settings live on the function, not in the invocation payload: a CA bundle is far too large to send on every call, and a caller must not be able to talk a bridge into accepting a weaker certificate.

The bits that are easy to get wrong

allowed_secret_arns has no default, on purpose. A bridge that can read every secret in the account is not a bridge, it is a skeleton key. List exactly the secrets it serves; Terraform rejects an empty list.

One bridge can serve several identities safely. Pools and drivers are cached by credentials, not just by address — two secrets resolving to different users on the same host get separate connections, and a rotated password opens a fresh one rather than reusing a connection authenticated with the old value.

The bridge must be in the VPC that can reach the database. Without subnet_ids it runs outside any VPC and cannot see a private database, which is the only reason it exists. The failure is a connection timeout, not a clear error.

Set the caller's invocation timeout above the bridge's. Otherwise the caller gives up while the query keeps running and holding a connection.

Reserve concurrency. Every execution holds database connections, so unbounded concurrency becomes unbounded connections on a database that is, by definition, hard to reach and probably not sized for it. Defaults to 10.


Behaviour worth knowing

A handler never throws. Query failures come back as { error }. An unhandled exception would reach the caller as a Lambda FunctionError, which is indistinguishable from the bridge itself being broken — so a typo in a WHERE clause would look like an outage.

Pools survive between invocations. Connections are cached at module scope for the life of the warm container. That is the opposite of the client's Lambda wrapper, which tears pools down on the way out — and deliberately so: this function exists to hold connections open, and it is one low-concurrency function rather than one instance per request path.

Explicit request fields beat the secret's. host, port and dbname from the caller win, so one cluster secret can serve several databases on that cluster. Identical to the rule the client applies when registering, so a database behaves the same whichever side resolves it.

Neo4j paging happens after the query. Cypher records are fetched, then sliced, so totalCount is a true total — but the whole result set crossed the wire to get it. A LIMIT in the Cypher is still the better tool.


Payload

The wire format is BridgePayload / BridgeResponse, imported from @speles7172/sql-client. Both sides therefore compile against one definition: a change to what the client sends fails to build here until the handler agrees.