# Workspace data

Your app can sign users in, but it has nothing of its own to store yet. This chapter gives the workspace its own data: a **second, isolated database** with its own Prisma schema, migration history, and generated client — separate from the primary Postgres the auth plugin migrated into. The reason to isolate it is the same reason teams isolate any domain: an independent lifecycle you can migrate, scale, and back up on its own. It also bounds the blast radius — a bad migration to your team schema cannot take the auth tables down with it, and vice versa.

What makes this a NetScript capability and not just "run two Postgres containers" is the **typed data layer**: one `netscript db add` scaffolds the datasource, joins it to the Aspire graph, and — after a single migrate/generate loop — hands you a fully typed Prisma client scoped to *that* datasource, with its own migration history. The isolation is real at the type level, not just the connection string, and the whole `db` toolchain is multi-database-aware, so every command takes a `--db` target rather than leaving you to juggle two schemas by hand.

1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/01-scaffold/)
2. [2 · Auth](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/02-auth/)
3. [3 · Workspace data](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/03-workspace-data/)
4. [4 · Provision job](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/04-provision-job/)
5. [5 · Route authz](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/05-route-authz/)
6. [6 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/06-deploy/)

## What you will build

A second Postgres datasource named `workspace`, scaffolded by `netscript db add`, with its own `Member` and `Workspace` models, its own migration, and its own typed Prisma client. By the end you can import that client at `database/workspace/schema/.generated/client.server.ts` and query workspace records — fully independent from the primary database and the auth tables.

## Before you begin

You need the auth layer from [chapter 2](https://rickylabs.github.io/netscript/tutorials/workspace/02-auth/) and **Aspire running**. The second database is provisioned as its own container in the Aspire graph, so Docker must be up too. Confirm the primary is migrated and Aspire is live:

```sh
# In my-workspace/, with `aspire start` up in another terminal
netscript db status        # primary datasource is migrated (chapter 2)
docker info                # Docker engine is running
```

> A second database is a deliberate choice
>
> A second datasource is the right tool when you need an
>
> isolated
>
> data domain — a separate migration lifecycle and a datasource you can scale and back up independently. If you only wanted a new
>
> table
>
> , you would add a model to the primary schema instead. Adding a whole datasource means a second migration history and a second generated client to manage. This track adds one on purpose, to keep workspace records cleanly separate from auth.

> This split is how a real NetScript app stores its data
>
> A production context-accumulator chat application built on NetScript uses exactly this shape: one
>
> org-catalog
>
> datasource (Prisma-managed: who exists, which projects and channels there are) kept separate from the datastores each channel accumulates context into. Not everything is one database. The catalog answers "what is there and who belongs to it"; the domain data lives on its own lifecycle. Your
>
> workspace
>
> datasource here plays the catalog role for your team records.

## Step 1 — Scaffold the second database

From the workspace root, run `netscript db add <engine>`. The `--name` flag sets the **config key** the datasource is registered under — use `workspace` so it does not collide with the primary `postgres`:

```sh
# Add a second Postgres for workspace records. Registered under
# NetScript.Databases.workspace, scaffolded into database/workspace/.
netscript db add postgres --name workspace
```

In one pass, `db add` scaffolds a workspace at `database/workspace/` (its own `schema/schema.prisma`, `prisma.config.ts`, and `scripts/`), registers the datasource in `appsettings.json` under `NetScript.Databases.workspace`, adds it as a project member, and regenerates the Aspire config so the new container joins the resource graph.

> The engine is swappable
>
> This track uses
>
> postgres
>
> , but
>
> db add
>
> is polyglot: swap
>
> postgres
>
> for
>
> mysql
>
> ,
>
> mssql
>
> , or
>
> sqlite
>
> to scaffold that engine instead. Postgres, MySQL, and SQL Server each provision an Aspire container; SQLite is file-backed and adds no container resource. Keep
>
> postgres
>
> to follow the rest of this tutorial as written.

> The new datasource starts empty
>
> db add
>
> scaffolds the workspace and registers the datasource, but it does
>
> not
>
> run a migration or generate a client — the new
>
> database/workspace/schema/schema.prisma
>
> ships a starter schema and no
>
> .generated/
>
> directory yet. You run the migration loop yourself in Step 4, targeting the new datasource with
>
> --db
>
> .

## Step 2 — Restart Aspire so the container joins the graph

Because `db add` regenerated the Aspire config, the new database becomes a container in the resource graph. Restart the AppHost so it provisions:

```sh
cd aspire
aspire start
```

Open the dashboard at [https://localhost:18888](https://localhost:18888) and confirm the new `workspace` resource goes green alongside the existing `postgres` and `redis`.

> Restart is not optional
>
> The new container only appears because
>
> db add
>
> regenerated the Aspire helpers
>
> . If you skip the restart, the AppHost will not know about the datasource and the resource never shows up — and the migration in Step 4 has nothing to reach.

## Step 3 — Define the workspace models

Edit the second datasource's schema to hold workspace records. A `Workspace` row is a team; a `Member` row links a signed-in user (by their auth `subject`) to a workspace:

```prisma
// database/workspace/schema/schema.prisma — add these models
model Workspace {
  id        String   @id @default(cuid())
  name      String
  createdAt DateTime @default(now())
  members   Member[]
}

model Member {
  id          String    @id @default(cuid())
  // The auth Principal.subject from chapter 2 — the stable user identifier.
  subject     String
  role        String    @default("member")
  workspace   Workspace @relation(fields: [workspaceId], references: [id])
  workspaceId String
  createdAt   DateTime  @default(now())

  @@unique([workspaceId, subject])
}
```

The `subject` column is the link back to auth: it stores the `Principal.subject` value the auth backend resolves for a signed-in user, so a workspace member is "this auth identity, in this workspace."

## Step 4 — 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`. Point each command at the `workspace` datasource. Run these from the workspace root with `aspire start` up:

```sh
# Create + apply the first migration for the SECOND datasource only.
netscript db init --db workspace --name init
```

```sh
# Generate the Deno-runtime Prisma client + zod schemas into
# database/workspace/schema/.generated for the workspace datasource.
netscript db generate --db workspace
```

```sh
# Confirm the workspace datasource is migrated and in sync.
netscript db status --db workspace
```

Each datasource keeps its **own** migration history and generated client. Migrating `--db workspace` never touches the primary Postgres (or the auth tables), and vice versa.

> Omitting --db targets the primary
>
> A
>
> db
>
> command with no
>
> --db
>
> resolves to the
>
> primary
>
> datasource (the default Postgres the auth plugin migrated into). Always pass
>
> --db workspace
>
> when you mean the second one, or
>
> --db all
>
> to fan out across every registered datasource.

## Step 5 — Query the workspace client

After `db generate --db workspace`, the second datasource has its own typed client. Import it exactly like the primary — just from the new path. It is an independent `PrismaClient`, typed off the `Workspace`/`Member` models:

```ts
// services/workspace/src/db.ts
// The SECOND datasource generates its OWN client. Import it from its path.
import { PrismaClient as WorkspacePrisma } from '../../database/workspace/schema/.generated/client.server.ts';

export const workspaceDb = new WorkspacePrisma();

// Fully typed off the workspace schema — separate from the primary client.
const teams = await workspaceDb.workspace.findMany({ take: 20 });
console.log(teams.length);
```

> Import the right client
>
> The primary client lives at
>
> database/postgres/schema/.generated/client.server.ts
>
> ; the workspace client at
>
> database/workspace/schema/.generated/client.server.ts
>
> . They are distinct
>
> PrismaClient
>
> s — crossing the imports queries the wrong database. Re-run
>
> netscript db generate --db workspace
>
> after every schema edit, or your code is typed against the old shape.

## Extend — app-level org scoping

The data model above is **single-tenant**: every `Member` belongs to a `Workspace`, but there is no framework-managed notion of an organization that owns many workspaces. NetScript does not ship orgs, tenants, or RBAC roles — if you want multi-tenant scoping, you add it yourself, in your own schema and your own queries.

> Multi-Tenancy and Org Scoping Design Boundary
>
> NetScript's identity system provides authentication (resolving a
>
> Principal
>
> with a
>
> subject
>
> and claims), but does not natively enforce organization boundaries or multi-tenant database scoping in the core runtime. This design boundary keeps authentication decoupled from application-level data isolation patterns. Today, multi-tenant scoping (such as filtering by
>
> orgId
>
> or managing role hierarchies) must be implemented in your application logic. Adding first-class, typed organization and multi-tenancy primitives is planned under roadmap item R3.

## Verify your progress

Confirm the second datasource is migrated and its client generated:

```sh
netscript db status --db workspace
ls database/workspace/schema/.generated/client.server.ts
```

`db status --db workspace` should report it migrated and in sync, and the generated client file should exist.

- [ ] `netscript db add postgres --name workspace` scaffolded `database/workspace/`.
- [ ] The `workspace` resource is green in the Aspire dashboard.
- [ ] `database/workspace/schema/schema.prisma` defines `Workspace` and `Member`.
- [ ] `netscript db status --db workspace` reports migrated and in sync.
- [ ] The generated client exists at `database/workspace/schema/.generated/client.server.ts`.

## What you built

Your workspace now owns its data: a second, isolated Postgres datasource with `Workspace` and `Member` models, its own migration history, and its own typed client — the same catalog-versus-domain split a real NetScript app runs on, cleanly separate from auth and the primary database. You also saw the boundary: org scoping is app-level, not a framework primitive. Next comes the moment this data model exists for — adding a member to a team — and you do it off the request path with a background job.

[2 · Auth](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/02-auth/) [4 · Provision job](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/04-provision-job/)
