# atdatabases: TypeScript database clients that block injection by design

> The @databases packages are TypeScript clients for Postgres, MySQL, SQLite, BigQuery and Expo, in which queries are written as tagged template literals and the library refuses anything else, so values are always sent to the engine separately from the SQL text. The trade-off is that you keep writing SQL by hand while losing the ability to build identifiers dynamically without extra tooling.

**ForbesLindesay/atdatabases** — TypeScript clients for databases that prevent SQL Injection

- Repository: https://github.com/ForbesLindesay/atdatabases
- Website: https://www.atdatabases.org
- Stars: 636 · Forks: 47
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/forbeslindesay-atdatabases

## The query is a template tag, not a string

The core idea of the @databases family is that you cannot write a query by concatenating strings, because the API does not accept a string. A query is written as a tagged template literal:

```ts
db.query(sql`SELECT * FROM users WHERE id=${userID}`);
```

The README states the security claim plainly, that using tagged template literals for queries makes it virtually impossible for SQL injection attacks to slip in un-noticed, and, importantly, that all the @databases libraries enforce the use of the sql tagged template literals, so you cannot accidentally miss them. That enforcement is the interesting part. A library that merely offers a safe helper still leaves the unsafe path open, and the difference is whether a reviewer has to check every call site or whether the type system does it.

What the library does with the template is the other half of the mechanism. The query is passed to the database engine as a separate string and values:

```js
{text: 'SELECT * FROM users WHERE id=?', values: [userID]}
```

So the interpolation never becomes part of the SQL text. The value travels alongside the statement, and the engine handles the parameter binding. The README calls this being safe from HTML injection, which is a slightly odd label for the risk, but the substance is SQL injection rather than markup.

The design has an obvious consequence that the package list makes visible. Values can be bound; identifiers cannot. A table name or a column name cannot be a bound parameter in SQL, so a query whose shape depends on the data needs a different tool, and the family includes @databases/escape-identifier precisely for that.

## A monorepo where every driver is its own npm package

The repository is a monorepo with a packages directory, driven by yarn workspaces over packages/*, and the published surface is a long list of separate modules. The README lists them: @databases/bigquery, @databases/cache, @databases/connection-pool, @databases/dataloader, @databases/escape-identifier, @databases/expo, @databases/lock, @databases/mysql, @databases/mysql-test, @databases/mysql-typed, @databases/pg, @databases/pg-bulk, @databases/pg-cluster, @databases/pg-migrations, @databases/pg-test, @databases/pg-typed, @databases/queue, @databases/split-sql-query, @databases/sql, @databases/sql-commenter, @databases/sqlite, @databases/sqlite-sync, @databases/validate-unicode, and more beyond the point where the list is cut off.

The README gives the reason for that split: each database driver is published to npm as a separate module, so you do not need to install the ones you do not need. The naming makes the granularity finer than that, though. Beyond one package per database, there are packages per supporting concern, a connection pool, a connection-pool module, a dataloader for batching, a queue, a lock, a cache, migrations, and test harnesses, plus typed variants of the drivers.

Two of those deserve a note. The typed variants, @databases/pg-typed and @databases/mysql-typed, are separate packages, so type safety at the row level is opt-in rather than baked into the base driver. And @databases/sql-commenter at version 1.0.0, released on 2026-08-19, is a newer addition whose name suggests commenting on SQL, which is the kind of tool that shows up when you want to log or explain a query without executing it.

The point of the monorepo is shared behaviour. The sql tagged template implementation and the query splitting logic live in shared packages, and the drivers build on them, which is what makes the enforcement consistent across Postgres, MySQL, SQLite, BigQuery and Expo rather than per driver.

## Promises and TypeScript as design constraints, not decorations

Two of the four properties the README lists are worth reading closely. The APIs are designed with promises in mind from the get go, which matters for a database client because the alternative is callback-shaped or event-emitter-shaped interfaces that force a wrapper for async and await.

The second is that the packages are written in TypeScript, so every module has type safety and type definitions built in. For a driver, the interesting types are not the function signatures but the shape of a row. A Postgres driver that returns any for every row is a driver that pushes the problem to the caller, and the separate typed packages are the answer to that, at the cost of one more module to install and a mapping between the query and the row type.

The remaining two properties are the injection guard and the modularity, both covered above. So the pitch in four lines is: safe by construction, promise based, typed, and install only what you use.

There is a fourth module family here worth naming, because it shows how the project thinks about a database client as more than a connector. @databases/connection-pool handles pooling, @databases/dataloader handles the batching pattern where many requests ask for the same key, @databases/queue handles a job queue backed by the database, @databases/lock provides distributed locking, @databases/cache provides caching, @databases/pg-migrations handles migrations, and @databases/pg-cluster handles a Postgres cluster. Each of those is a piece of infrastructure a production Node service tends to need, packaged under the same name and versioned separately.

The repository also carries a .mysqlrc.json and a .pgrc.json, crowdin.yaml for translations of the documentation, an expo-test-project directory, a scripts directory, a docs directory, a SECURITY.md and a LICENSE.md.

## The test matrix runs against real Postgres and MySQL images

The root package.json is private and holds the workspace scripts, and the test setup is the part worth reading. The test script runs jest with the node, pg and mysql projects selected, with experimental VM modules enabled and the timezone pinned to Australia/Adelaide:

```json
    "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" TZ=Australia/Adelaide jest --selectProjects node --selectProjects pg --selectProjects mysql",
    "test:node": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" TZ=Australia/Adelaide jest --selectProjects node",
```

The per-version scripts set the image the tests run against, so the same suite is executed against several server versions:

```json
    "test:pg:14": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" TZ=Australia/Adelaide PG_TEST_IMAGE=postgres:14.23-alpine jest --selectProjects pg",
    "test:pg:18": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" TZ=Australia/Adelaide PG_TEST_IMAGE=postgres:18.4-alpine jest --selectProjects pg",
```

Pinning the timezone to a fixed offset is a small thing that says a lot about how the tests are built. A test that formats a timestamp or computes a date boundary behaves differently in a different zone, and fixing the zone makes the suite deterministic wherever it runs. The same scripts exist for MySQL, with MYSQL_TEST_IMAGE set to specific versions such as mysql:5.6.51, which means the driver is checked against server versions that are old enough to still be in use.

The build side is a TypeScript project build, tsc --build scripts, and formatting is checked with prettier over the package sources. The CI configuration lives in a .circleci directory alongside the GitHub workflows, and the whole thing runs on yarn workspaces, so contributions go through the same lint, format and test path as any other change in the tree.

## Values are safe, identifiers are not

The limitation of the whole approach follows from how SQL parameter binding works, and it is worth being precise about it, because the README's claim is strong enough to invite misreading.

Bound values cover the overwhelming majority of injection risk. Anything a user types into a form field, a query parameter or a JSON body can be interpolated into the template and it will be sent as a value, never as text. That is where injection lives, and the guard covers it.

Identifiers are the gap. A table name, a column name, the direction of an ORDER BY clause or a list of columns in a SELECT cannot be bound as parameters in SQL, so a query whose shape comes from user input has to build the statement text. The @databases family does not forbid that outright, it supplies @databases/escape-identifier for it, which is the honest answer to the problem, but it is a different mechanism from the template guard, and it is one you have to reach for deliberately.

Two other things follow from the same architecture. First, because the guard lives in the template, queries that are not templates are not queries, so anything that wants to build SQL dynamically, a report builder, a query builder, an admin tool with user-defined filters, has to go around the safe path. The presence of @databases/split-sql-query in the package list suggests the project supports splitting a SQL script into individual statements, which is what a migration runner or a seeder needs.

Second, the approach gives you no query planner and no schema layer. You write SQL, and the library gives you a safe way to pass values. That is a deliberate boundary: the family is a set of connectors with a security property, not an ORM, and the typed row packages are the closest thing to a schema layer on offer.

## The alternative is a plain driver, and the difference is the guardrail

The obvious alternative is to use the underlying driver directly, node-postgres for Postgres or the MySQL client, and write your own parameterised queries. The difference in approach is narrow but real, and it is entirely about enforcement.

A raw driver accepts a text and a values array, and it also accepts a concatenated string, because that is a legitimate thing to want sometimes. Nothing stops a developer from writing SELECT * FROM users WHERE id = ' + userID, and code review is the only defence. The @databases libraries remove that path, since the query method takes the template type and a string will not typecheck. The cost is that the family is opinionated about how you call it, and that a codebase migrating in from a raw driver has to convert its queries rather than swap an import.

The other alternative is an ORM, and there the difference is larger. An ORM generates SQL from an object model and owns the query layer, which removes hand-written SQL entirely but also removes the ability to write the exact statement you need when the generated one does not fit. The @databases family sits deliberately in between: SQL is written by you, parameterisation is enforced for you.

The release record shows the family still moving. @databases/sqlite-sync was published as 3.1.0 on 2026-09-29, and on 2026-08-19 the same day saw @databases/sql-commenter at 1.0.0 and @databases/pg at 6.1.0, which is what a monorepo releasing many packages on one day looks like. The licence is MIT, recorded in LICENSE.md, which for a set of database clients means you can vendor the drivers into a proprietary product, provided the copyright and licence notice is kept.

## Conclusion

atdatabases suits a TypeScript team that wants to keep hand-written SQL and still make string concatenation into queries impossible at the type level, and it suits a team that wants an ORM with generated queries less well. Before adopting it, check that the drivers you need are on the list, since each database is a separate npm package, and remember that the template guard covers values only, so any query with a dynamic table name or column needs @databases/escape-identifier. The project is MIT licensed, the newest package release was @databases/sqlite-sync 3.1.0 on 2026-09-29, and the last push was on 2026-09-29.

## FAQ

### How do the @databases packages prevent SQL injection?

Queries are written as tagged template literals, for example db.query(sql`SELECT * FROM users WHERE id=${userID}`), and the library passes the statement text and the values to the engine separately. The README states that all the @databases libraries enforce the use of the sql tagged template literals.

### Which databases do the @databases packages support?

The package list includes @databases/pg for Postgres, @databases/mysql, @databases/sqlite, @databases/sqlite-sync, @databases/bigquery and @databases/expo for Expo and WebSQL, along with typed variants and support packages such as connection-pool, dataloader, queue, lock, cache and migrations.

### Can I use a dynamic table or column name with @databases?

Identifiers cannot be bound as parameters in SQL, so a query with a dynamic shape needs another mechanism, and the family includes @databases/escape-identifier for that case. The template guard protects interpolated values.

### How does atdatabases test against database versions?

The root package.json defines jest scripts that set PG_TEST_IMAGE for specific Postgres images such as postgres:18.4-alpine and MYSQL_TEST_IMAGE for MySQL versions such as mysql:5.6.51, with the timezone pinned to Australia/Adelaide so the suite behaves the same everywhere.

### What licence does atdatabases use?

MIT, with the terms in the LICENSE.md file at the root of the repository. Each driver and helper is published separately to npm under the @databases scope.

## Sources

- [ForbesLindesay/atdatabases on GitHub](https://github.com/ForbesLindesay/atdatabases)
- [License: MIT](https://github.com/ForbesLindesay/atdatabases/blob/master/LICENSE)
- [Project website](https://www.atdatabases.org)
- [README](https://github.com/ForbesLindesay/atdatabases/blob/master/README.md)
- [Releases](https://github.com/ForbesLindesay/atdatabases/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/forbeslindesay-atdatabases
