Use a second database
Goal: add a second database — a second Postgres, or a MySQL/SQL Server instance — to a NetScript workspace that already has its primary datasource, so each datasource gets its own Prisma schema, migrations, and generated client.
This recipe assumes the primary datasource is Postgres (the recommended default — every tutorial
scaffolds with --db postgres), but the primary engine is itself chosen at scaffold time: pass
--db mysql, --db mssql, or --db sqlite to netscript init for a MySQL, SQL Server, or
file-backed SQLite primary instead. Everything below applies regardless of which engine your
primary uses; the examples simply show the common Postgres-primary case.
NetScript's default scaffold gives you one primary datasource that every plugin
aggregates its .prisma models into (see Database & migration).
A second database is the opposite shape: a separate Prisma schema workspace with its own
generate output and its own migration history. It never merges into the primary aggregation.
The second datasource is polyglot the same way the primary is: netscript db add <engine>
accepts the same four engines as netscript init — postgres, mysql, mssql, and sqlite.
The container-mode engines (postgres/mysql/mssql) are provisioned as an Aspire container
resource (addPostgres / addMySql / addSqlServer); sqlite is file-backed and has no
Aspire container resource, so the Aspire/Docker prerequisites below apply only to the
container engines.
There are two ways to add one, and they answer different needs:
- Scaffold it (
netscript db add) — when you want NetScript to own the second datasource: its own schema dir, migrations, seed scripts, and an Aspire-provisioned container. This is the recommended path and the bulk of this recipe. - Wire an adapter by hand — when the second database is external (a managed MySQL, a reporting warehouse) that you only read/write from application code, with no NetScript-managed migrations. Covered in Connect an external database by hand.
Before you start
| Name | Type | Description |
|---|---|---|
A scaffolded workspace |
with a primary db |
An existing NetScript project whose primary datasource is already wired (Postgres, the recommended engine, or whichever engine you passed to netscript init via --db) — ideally migrated once via the Database & migration recipe so you know the single-datasource loop. |
netscript CLI |
on PATH |
deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.7. netscript db add --help should print. |
Aspire CLI + Docker |
for container mode |
The scaffolded second database is provisioned as a container by Aspire (addMySql / addPostgres / addSqlServer). Docker or Podman must be running. Skip only for an external/hand-wired database. |
Deno |
2.x |
Prisma client generation runs under the Deno runtime (the generated schema sets runtime="deno"). |
Step 1 — Scaffold the second database
From the workspace root, run netscript db add <engine>. The engine is one of
postgres, mysql, mssql, or sqlite. The --name flag sets the config key the
datasource is registered under (it defaults to the engine name):
# Add a MySQL datasource. Registered under NetScript.Databases.mysql,
# scaffolded into database/mysql/, provisioned by Aspire's addMySql (image 8.4).
netscript db add mysql
# A second Postgres for an isolated domain. --name gives it a distinct config
# key (and workspace), so it does not collide with the primary 'postgres'.
netscript db add postgres --name analytics
# SQL Server via Aspire's addSqlServer. Same shape — its own schema + migrations.
netscript db add mssql --name reporting
What netscript db add does, in one pass:
- Scaffolds a workspace at
database/<engine>/(formysql,database/mysql/) — its ownschema/schema.prisma,prisma.config.ts,scripts/, and (after generate)schema/.generated/. - Registers the datasource in
appsettings.jsonunderNetScript.Databases.<configKey>with the engine, mode, and a generatedDatabaseName— the same appsettings-driven model the primary Postgres uses. - Adds the workspace as a member of the project so tooling discovers it.
- Regenerates the Aspire config and AppHost helper files so the new container (for example
the MySQL resource via
addMySql) joins the resource graph the next time you runaspire start.
Step 2 — Bring the new container up with Aspire
Because db add regenerated the Aspire config, the new database becomes a container in the
resource graph. Start (or restart) the AppHost so it provisions:
# database/migration recipe covers this in full — restart so Aspire picks up
# the regenerated config and provisions the new container (e.g. the mysql resource).
cd aspire
aspire start
Open the Aspire dashboard at https://localhost:18888 (the access
token is printed by aspire start) and confirm the new resource — mysql, analytics, or
whatever your config key is — goes green alongside the existing postgres and redis.
Step 3 — Migrate and generate the second datasource
The netscript db operations are multi-database aware: every one takes a --db <target>
flag, where the target is a config key, a database name, or all. With the second database
registered under NetScript.Databases.mysql, point each command at it with --db mysql. Run
these from the workspace root, with aspire start up in another terminal:
# Create + apply the first migration for the SECOND datasource only.
# --db selects the config key; --name labels the migration directory.
netscript db init --db mysql --name init
# Generate the Deno-runtime Prisma client + zod schemas into
# database/mysql/schema/.generated for the mysql datasource.
netscript db generate --db mysql
# Run that datasource's seed scripts (database/mysql/scripts/seed.ts).
netscript db seed --db mysql
# Confirm the mysql datasource is migrated and in sync — the authoritative check.
netscript db status --db mysql
# Operate on EVERY registered datasource at once (primary + second).
netscript db migrate --db all --name add_reports
netscript db generate --db all
Each datasource keeps its own migration history under database/<engine>/migrations/
and its own generated client. Editing the second schema and re-running
netscript db migrate --db mysql never touches the primary Postgres, and vice versa.
Step 4 — Query the second client from application code
After db generate --db mysql, the second datasource has its own typed client at
database/mysql/schema/.generated/client.server.ts. Import it exactly like the primary —
just from the new path. The two clients are independent PrismaClient instances, so a service
can read from both:
// services/reporting/src/db.ts
// Each datasource generates its OWN client. Import the second one from its path.
import { PrismaClient as ReportingPrisma } from '../database/mysql/schema/.generated/client.server.ts';
export const reporting = new ReportingPrisma();
// Fully typed off the SECOND schema's models — separate from the primary client.
const rows = await reporting.report.findMany({ take: 20 });
console.log(rows.length);
// services/reporting/src/sync.ts
// A service can hold both clients side by side — one per datasource.
import { PrismaClient as AppPrisma } from '../database/postgres/schema/.generated/client.server.ts';
import { PrismaClient as ReportingPrisma } from '../database/mysql/schema/.generated/client.server.ts';
const app = new AppPrisma();
const reporting = new ReportingPrisma();
// Copy a record from the primary Postgres into the MySQL reporting datasource.
const order = await app.exampleRecord.findFirstOrThrow();
await reporting.report.create({ data: { sourceId: order.id, name: order.name } });
Connect an external database by hand
If the second database is external — a managed MySQL you do not want NetScript to migrate
or provision — skip db add and wire a driver adapter in application code. NetScript wraps each
Prisma 7 driver in a small DatabaseAdapter with a uniform lifecycle
(getDriverAdapter → setClient → connect/healthCheck/getStatus). The MySQL and SQL Server
adapters are sub-exports (not in the @netscript/database/adapters barrel), so a Postgres-only
app never pulls in their drivers:
| Name | Type | Description |
|---|---|---|
createMysqlAdapter(opts) |
@netscript/database/adapters/mysql |
MySQL 8.x / MariaDB via the native-Deno @netscript/prisma-adapter-mysql driver. MysqlConnectionOptions adds charset, timezone, connectionLimit, multipleStatements over the shared parts. |
createMssqlAdapter(opts) |
@netscript/database/adapters/mssql |
SQL Server via @prisma/adapter-mssql. MssqlConnectionOptions adds instanceName, encrypt, trustServerCertificate, integratedSecurity, connectTimeout, requestTimeout. |
createPostgresAdapter(opts) |
@netscript/database/adapters |
PostgreSQL via @prisma/adapter-pg. The only adapter in the barrel; PostgresConnectionOptions adds schema and applicationName. |
The shared options come from DatabaseConnectionOptions (@netscript/database/ports): pass a
connectionString, or the structured host / port / database / username / password /
ssl / poolSize / timeout parts.
// services/reporting/src/external-db.ts
// Sub-export — import from /adapters/mysql, NOT the barrel.
import { createMysqlAdapter } from '@netscript/database/adapters/mysql';
import { PrismaClient } from '../database/mysql/schema/.generated/client.server.ts';
// 1) Build the adapter from structured parts (or pass { connectionString }).
const adapter = createMysqlAdapter({
host: Deno.env.get('MYSQL_HOST') ?? 'localhost',
port: 3306,
database: 'reporting',
username: Deno.env.get('MYSQL_USER') ?? 'root',
password: Deno.env.get('MYSQL_PASSWORD'),
ssl: false,
});
// 2) Pass the driver adapter into Prisma, then hand the client BACK to the adapter.
export const reporting = new PrismaClient({ adapter: adapter.getDriverAdapter() });
adapter.setClient(reporting);
// 3) Lifecycle + health now run off the same client.
await adapter.connect();
const ok = await adapter.healthCheck(); // SELECT 1
console.log('mysql healthy:', ok, await adapter.getStatus());
// Prefer reading config from the environment? getMysqlConfig() reads structured
// MYSQL_HOST / MYSQL_PORT / MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD vars and
// falls back to a connection-string env var (MYSQLDB_URI, then DATABASE_URL).
import { createMysqlAdapter, getMysqlConfig } from '@netscript/database/adapters/mysql';
const cfg = getMysqlConfig(); // → MysqlAdapterConfig, or throws if nothing is set
const adapter = createMysqlAdapter({
host: cfg.hostname,
port: cfg.port,
database: cfg.db,
username: cfg.username,
password: cfg.password,
});
// (getMssqlConfig has the same shape for SQL Server, reading MSSQL_* / MSSQLDB_URI.)
// services/reporting/src/external-db.ts — SQL Server (same lifecycle)
import { createMssqlAdapter } from '@netscript/database/adapters/mssql';
import { PrismaClient } from '../database/mssql/schema/.generated/client.server.ts';
const adapter = createMssqlAdapter({
host: Deno.env.get('MSSQL_SERVER') ?? 'localhost',
port: 1433,
database: 'reporting',
username: 'sa',
password: Deno.env.get('MSSQL_PASSWORD'),
encrypt: true, // local-dev TLS knobs; tighten for production
trustServerCertificate: true,
});
export const reporting = new PrismaClient({ adapter: adapter.getDriverAdapter() });
adapter.setClient(reporting);
await adapter.connect();
Unsupported by NetScript, supported by Prisma (libSQL / Turso example)
When you need a backing database that Prisma supports (such as libSQL / Turso via @prisma/adapter-libsql, PlanetScale, or Cloudflare D1) but NetScript does not ship a pre-packaged wrapper for, wire Prisma's driver adapter directly in application code.
1. Distinguishing Prisma's driver adapter from NetScript's DatabaseAdapter
It is important to distinguish the two levels of adapters:
- Prisma driver adapter (
PrismaLibSql): Low-level engine adapter provided by upstream (@prisma/adapter-libsql). Its sole job is bridging Prisma 7 query engine calls to the underlying driver (e.g.@libsql/client). You pass this directly tonew PrismaClient({ adapter }). - NetScript
DatabaseAdapterwrapper: High-level framework port (@netscript/database/ports) implemented by NetScript's factories (createPostgresAdapter,createMysqlAdapter,createMssqlAdapter). It encapsulates connection lifecycle (connect(),disconnect()), health checks (healthCheck()), status metrics (getStatus()), and raw query dispatch (executeRaw()).
When using an unsupported driver like libSQL/Turso directly, your application uses Prisma's driver adapter without implementing NetScript's DatabaseAdapter.
2. Code example (libSQL / Turso)
Below is a complete, pasteable example using @prisma/adapter-libsql and the generated Deno Prisma client shape:
// database/turso/mod.ts (or services/chat/src/db.ts)
import { PrismaLibSql } from '@prisma/adapter-libsql';
import { PrismaClient as TursoClient } from './schema/.generated/client.server.ts';
// 1. Resolve environment variables under Deno
const connectionString = Deno.env.get('TURSO_DATABASE_URL') ?? 'libsql://my-app.turso.io';
const authToken = Deno.env.get('TURSO_AUTH_TOKEN');
// 2. Construct Prisma's driver adapter directly
const adapter = new PrismaLibSql({
url: connectionString,
authToken,
});
// 3. Instantiate the generated Deno Prisma client with the driver adapter
export const turso = new TursoClient({ adapter });
// 4. Application-owned health check ping
export async function checkTursoHealth(): Promise<boolean> {
try {
await turso.$queryRaw`SELECT 1`;
return true;
} catch {
return false;
}
}
3. Application-owned responsibilities
Because NetScript does not wrap the driver, the application owns the following responsibilities:
- Client generation & output shape: Configure
schema/schema.prismawithgenerator client { provider = "prisma-client", output = "./.generated", runtime = "deno" }. Runnetscript db generate --db <name>to output the Deno-compatible client at./schema/.generated/client.server.ts. - Environment & connection management: Resolve URL and credentials (such as
TURSO_DATABASE_URLandTURSO_AUTH_TOKEN) viaDeno.env.get(). - Lifecycle & health: Manage startup/shutdown (
turso.$connect(),turso.$disconnect()) and implement custom health checks (e.g.turso.$queryRawSELECT 1``) for readiness probes or health reporting. - Tracing & telemetry: Configure query logging or OpenTelemetry tracing directly on the
PrismaClientinstance if desired. - Permissions (driver-dependent): Grant the specific Deno runtime permissions required by your database driver (
--allow-net,--allow-env,--allow-readfor network/cloud drivers like libSQL over HTTP;--allow-ffiis driver-dependent and only needed if using native/C bindings), and declare required permissions on Aspire apphost entries when launching under Aspire.
4. Decision rule: Direct use vs. reusable DatabaseAdapter wrapper
- Use direct Prisma driver adapter (
new PrismaClient({ adapter })) app-locally when:- You are adding a single service/datasource with an upstream driver (e.g. libSQL/Turso).
- Standard
PrismaClientmethods satisfy all query needs. - You do not need centralized
netscript dblifecycle hooks or multi-plugin status reporting.
- Implement a reusable NetScript
DatabaseAdapterwrapper when:- The database driver will be shared across multiple plugins or workspace packages.
- You need uniform
connect(),disconnect(),healthCheck(), andgetStatus()behavior across the workspace. - CLI tools or plugin doctor require standardized status reporting.
In-production pitfalls
See also
Database & Prisma — capability hub ·
Database & migration — the primary-datasource loop ·
@netscript/database reference ·