Give Every Customer Their Own Database
Provision isolated Turso databases for each customer and query their invoices.
Introduction
30 Second Summary
Keeping every customer's records together means each lookup must select the right customer. One missed check can expose the wrong records.
In this project, you will build a TypeScript service that creates a separate Turso database whenever a customer signs up. You will query each customer's invoices through that customer's own connection to prove the records stay separate without a tenant filter.
What You'll Build
You will run the same invoice lookup through two customer connections to see each response contain only that customer's rows.
By the end of this project, you'll have:
- A customer onboarding flow that turns each signup into a seeded database visible in your Turso dashboard.
- An isolation proof where two customer databases return different invoice rows.
- A reusable invoice query built with listInvoices that returns one customer's invoices without a tenant filter.
- Secret Mission: Extend the isolation proof to a third customer with a unique invoice.
Are there any prerequisites?
You need Node.js installed on your computer plus basic TypeScript knowledge. The first step guides you through creating a Turso account.
Before We Start
Every customer database in this project depends on code that you can install and run locally. Preparing a clean workspace now keeps environment issues separate from the database work ahead.
You will prepare Node.js with npm. You will also initialize an empty TypeScript project named tenant-databases.
In this step, get ready to:
- Confirm that Node.js is available in your terminal.
- Create the tenant-databases project on your Desktop.
- Configure the TypeScript development tools.
Check Node.js and npm
Node.js runs the development tools used throughout this project. npm installs the packages recorded in package.json.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Terminal on macOS or PowerShell on Windows.
- Press Enter to open the terminal.
- Check the installed Node.js and npm versions by running:
node --version
npm --version
What do these commands check?
- The first command asks Node.js to print its installed version.
- The second command asks npm to print its installed version.
✔️ I see two version numbers
Your local runtime is ready. Those two version numbers confirm that Node.js and npm are available in this terminal.
ⓧ A command is not found
The Node.js installer includes npm. Installing the release labelled LTS prepares both tools.
- Visit the official Node.js download page.
- Download the installer labelled LTS for your operating system.
- Run the downloaded installer.
- Keep the default installation options.
- Close the current terminal after the installation finishes.
- Use system search to open a fresh terminal.
- Confirm the installation by running:
node --version
npm --version
Each command prints a version number when the installation is available in your fresh terminal.
Still missing a command?
Restart your computer if the fresh terminal cannot find Node.js. This refreshes the system paths used to locate installed commands.
If the command remains unavailable, reinstall the LTS release for your operating system. Help me diagnose my Node.js installation.
Create the local TypeScript project
Your project needs one predictable location for its package settings and future source code. The Desktop keeps the new folder easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
This changes the terminal location to your Desktop. The project folder you create next will live there.
- Create the project skeleton by running these commands:
mkdir tenant-databases
cd tenant-databases
npm init -y
mkdir src
ls
What did these commands create?
- The tenant-databases folder holds the local project.
- The package.json file records project settings and packages.
- The empty src directory becomes the home for application code.
- The final command lists the new project artifacts in your terminal.
The project skeleton is in place. You should see package.json and src in the terminal output.
Project folder already exists?
An existing tenant-databases folder can prevent the first command from creating a clean project. Preserve any work in that folder before repeating the setup.
Use a different folder only after confirming that the existing project is safe. Help me resolve the folder conflict safely.
The project now needs a compiler for type checking. It also needs tsx to run TypeScript files during development.
- Install the TypeScript development tools by running:
npm install --save-dev typescript@7.0.2 tsx@4.23.15
What did npm install?
- TypeScript 7.0.2 provides the compiler and type checker.
- tsx 4.23.15 runs TypeScript files during development.
- The development dependency flag records both tools under devDependencies in package.json.
- Confirm that npm recorded both development tools by running:
npm pkg get devDependencies
What should I see?
The output should list typescript and tsx with their saved versions. This proves that npm added both tools to package.json.
Package installation failed?
Confirm that the terminal is inside the tenant-databases folder. A network interruption can also stop npm before every package downloads.
Read the first error in the npm output before retrying. Help me diagnose the npm installation error.
Configure and verify the project
npm scripts give the project consistent commands for development and compilation. The TypeScript configuration defines how files inside src are compiled.
- Replace the placeholder npm script with the project scripts by running:
npm pkg delete scripts.test
npm pkg set private=true --json
npm pkg set type=module scripts.dev="tsx src/index.ts" scripts.build=tsc
npm pkg get scripts
What do these settings control?
- Removing the placeholder test script leaves commands this project uses.
- The private setting protects the local project from accidental publication.
- The dev script is ready to run src/index.ts after application code is added.
- The build script runs the TypeScript compiler.
The final command prints the new dev and build scripts. Your project now has stable commands for later steps.
Scripts missing from the output?
Check that the terminal prompt points to the tenant-databases folder. The commands update the package.json file in the current location.
Compare each script name with the command above. Help me repair the npm scripts.
- Create tsconfig.json for the src directory by running:
npx tsc --init --rootDir src --outDir dist --module NodeNext --target ES2022 --strict
What does the TypeScript configuration do?
- The source directory setting keeps compiler input inside src.
- The output directory setting sends compiled JavaScript to dist.
- The module setting prepares imports for the Node.js runtime.
- Strict checking catches unsafe values during compilation.
Before you run the final checkpoint, predict which files and folders the terminal will list.
- Verify the project structure and npm scripts by running:
ls
npm pkg get scripts
ls src
What should I see?
The first output should include package.json, src, and tsconfig.json.
The scripts output should contain dev and build. The final command should list no source files because src is empty.
✔️ Awesome, I've got everything!
Your project foundation is complete. Keep this terminal open inside tenant-databases for the next step.
ⓧ I'd like to double check the full code
Compare your project with this exact setup checkpoint.
- package.json: Initialized npm project for tenant-databases with TypeScript development dependencies and scripts for running TypeScript code.
- src/: Empty source directory.
- tsconfig.json: TypeScript configuration for compiling source files in src.
Your local workspace is ready for customer database work. Next, you will prepare the template database in Turso.
Create the Tenant Template
Each customer database needs a dependable starting point. A template gives every future customer the same foundation.
In this step, you will use Turso to create a group named tenants. You will place an empty tenant-template database inside it.
In this step, get ready to:
- Access the Turso dashboard with your new account.
- Create the tenants database group.
- Create an empty tenant-template database.
Create your Turso account
Your account gives you a dashboard for managing the databases used by this project.
Account setup can feel like a billing commitment. This project stays within Turso's free database allowance.
- Open the Turso dashboard in your browser.
- Choose the account creation option shown on the page.
- Complete the prompted authentication flow.
- Choose the free account option if a plan screen appears.
You're in. The signed-in dashboard confirms that your Turso account is ready.
Create the tenants group
A database group keeps related databases under one location policy. Every customer database in this project belongs to the tenants group.
- Open the group selector beside your current organization.
- Choose the option to create a group.
- Enter tenants in the group name field.
- Select the available primary location nearest to you.
- Create the group with the dashboard confirmation control.
You'll see tenants in the group selector. That visible entry confirms the group exists.
What does the group control?
A Turso group defines where its databases live. Creating tenants first gives every customer database a shared home.
The group also gives your provisioning code a precise destination later in the project.
Can't create the group?
- Confirm that the dashboard shows your signed-in account.
- Confirm that tenants uses lowercase letters.
- Refresh the dashboard if the group creation control remains unavailable.
Ask for help with creating the Turso group.
Create the empty template database
The template database becomes the source that future customer databases copy. This checkpoint keeps it empty before the next step adds the invoice structure.
- Switch from group management to the database list for tenants.
- Choose the option to create a database.
- Enter tenant-template in the database name field.
- Select tenants in the group field.
- Choose the empty database option if initialization choices appear.
- Confirm the database creation.
Before you check the database list, which database name do you expect to see inside tenants?
- Return to the database list to verify the result.
You'll see tenant-template listed in the tenants group. Your reusable database foundation is now in place.
Database missing from the list?
- Confirm that the database list is showing the tenants group.
- Refresh the dashboard to request the latest database list.
- Repeat the creation flow if the earlier form did not finish.
Ask for help with finding the template database.
Your empty template is ready. Next, you'll add the invoice structure plus sample data.
Seed the Template Database
Your Turso group now contains the empty tenant-template database. An empty template would give every new customer a database with no useful structure.
In this step, you will use SQL to turn the template into an invoice blueprint. A final query will prove that its sample invoice is ready for future customer databases.
In this step, get ready to:
- Create the invoices table in tenant-template.
- Seed the table with a sample paid invoice.
- Query the template to confirm the sample row.
Create the invoices table
A table schema defines the shape of every invoice stored in the template. Each future database can start with the same four fields.
- Switch back to the Turso dashboard from earlier.
- Select tenant-template inside the tenants group.
- Select Edit Data on the database page.
- Select Outerbase Studio to reach the SQL editor.
- Replace the editor contents with this table definition:
-- Define the invoice shape copied into future customer databases.
CREATE TABLE invoices (
id TEXT PRIMARY KEY,
customer_name TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
status TEXT NOT NULL
);
What does this table define?
- The id column stores a durable invoice identifier.
- The customer_name column identifies the customer shown on the invoice.
- The amount_cents column stores the amount as a whole number of cents.
- The status column records the invoice state.
- The PRIMARY KEY constraint keeps invoice identifiers unique.
- The NOT NULL constraints make each invoice field required.
- Execute the statement with the run control in the editor toolbar.
- Refresh the table list in Outerbase Studio.
You should see an invoices table with four columns. That table is now the shared invoice structure for every database copied from this template.
Table missing from the list?
- Verify that tenant-template remains selected in the database header.
- Check the statement for a missing parenthesis.
- Execute the corrected statement again.
If the table still does not appear, help me debug this table creation statement.
Seed the sample invoice
Seed data gives the template a known baseline row. That row makes each later database copy easy to inspect.
- Replace the SQL editor contents with this insert statement:
-- Add a known invoice so future copies have a visible baseline.
INSERT INTO invoices (id, customer_name, amount_cents, status)
VALUES ('INV-TEMPLATE-001', 'Template Customer', 10000, 'paid');
What does this seed row represent?
- The INV-TEMPLATE-001 value gives the baseline invoice a recognizable identifier.
- The Template Customer value distinguishes this row as template data.
- The 10000 value represents $100.00 in cents.
- The paid value gives the invoice a completed status.
- Execute the insert statement with the run control in the editor toolbar.
The editor should report a successful write for one row. The template now contains a concrete invoice for the final check.
Invoice not inserted?
- Confirm the table name is exactly invoices.
- Continue to the query below if a retry reports that the invoice already exists.
If the write still fails, help me debug this invoice insert.
Query the seeded invoice
A read query verifies the complete template state. It checks the schema by requesting every required column.
- Replace the SQL editor contents with this verification query:
-- Confirm the template contains the expected baseline invoice.
SELECT id, customer_name, amount_cents, status
FROM invoices;
What does this query check?
The query reads all four required columns from invoices. Listing the columns explicitly makes the expected result order clear.
Before you execute this query, which invoice do you expect the template to return?
- Execute the verification query with the run control in the editor toolbar.
You should see one row with id set to INV-TEMPLATE-001. Its customer_name value is Template Customer.
The amount_cents value is 10000. The status value is paid.
You have completed the groundwork that makes the next customer database meaningful. The template now carries a proven invoice structure with a known baseline row.
Query returned no rows?
- Confirm the query still targets the invoices table.
- Return to the seed substep if the result is empty.
- Execute the insert statement once before retrying the query.
If the columns or values differ, help me compare my query result with the expected template invoice.
Your template database is ready for copying. Next up, you will provision the first customer database from this seeded blueprint.
Provision Your First Tenant
Your template database now holds the shared invoice structure plus a sample row. It can become the starting point for every customer.
Manual database copying cannot keep up with customer signups. In this step, you will use the Turso Platform API to turn that copy operation into a TypeScript function.
In this step, get ready to:
- Create a Platform API token.
- Write the provisionTenant function.
- Provision customer-acme from tenant-template.
Create and store a Platform API token
A Platform API token lets your code manage Turso resources. The token appears only once after creation.
This credential can create databases in your account. Local storage keeps it away from your application code.
- Switch back to the signed-in Turso dashboard from earlier.
- Open your account settings from the account menu.
- Select Create Platform API Token.
- Enter tenant-provisioner as the token name.
- Set the token scope to the organization that contains the tenants group.
- Select Create Token.
- Copy the token before closing the token window.
- Record the selected organization slug here: your Turso organization slug.
Why does the token stay local?
The Platform API uses the token to authorize management requests. Keeping it in .env prevents the credential from becoming part of provisionTenant.ts.
Keep the token out of screenshots. Anyone with the token could make authorized requests from your account.
- Create .env inside tenant-databases using your code editor's new-file control.
You should now see .env in the file sidebar.
- Add the local configuration below to .env:
TURSO_ORG=[[TURSO_ORG="your Turso organization slug"]]
TURSO_PLATFORM_TOKEN=your-platform-api-token-here
What does this configuration hold?
- The TURSO_ORG value tells the API client which account owns your databases.
- The TURSO_PLATFORM_TOKEN value authorizes the provisioning request.
- Replace your-platform-api-token-here with the token you copied.
- Save .env.
The unsaved indicator beside .env disappears. Your credential is now stored locally.
- Keep .env out of every source-control commit.
Build provisionTenant
The official API client sends database management requests. Dotenv loads your local .env values into process.env.
- Install both dependencies by running this command in the open terminal:
npm install @tursodatabase/api dotenv --save
What did this install?
- The @tursodatabase/api package provides the client that creates the tenant database.
- The dotenv package loads your local configuration before the client starts.
You should see npm finish without an installation failure. The dependencies are now recorded in package.json.
Dependency installation failed?
Confirm the terminal is still inside tenant-databases. A different folder may not contain the expected package.json.
Check your internet connection if the package download stops. Run the installation command again after the connection recovers.
Help me diagnose my npm installation failure.
- Create src/provisionTenant.ts inside tenant-databases using your editor's new-file control.
- Add the provisioning module below to src/provisionTenant.ts:
import "dotenv/config";
import { createClient } from "@tursodatabase/api";
// Authenticate management requests with local environment values.
const turso = createClient({
org: process.env.TURSO_ORG!,
token: process.env.TURSO_PLATFORM_TOKEN!,
});
export async function provisionTenant(customerSlug: string) {
const databaseName = `customer-${customerSlug}`;
// Copy the template so this tenant starts with shared schema and seed data.
return turso.databases.create(databaseName, {
group: "tenants",
seed: {
type: "database",
name: "tenant-template",
},
});
}
// Provision the first customer and surface a concrete success signal.
async function main() {
await provisionTenant("acme");
console.log("Provisioned customer-acme.");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
What does this code do?
- The dotenv/config import loads the values from .env.
- The createClient() call authenticates Platform API requests.
- The databaseName value adds the customer slug to the customer- prefix.
- The group option places every new database in tenants.
- The seed option copies the schema plus the existing data from tenant-template.
- The main() function provisions the first customer with the slug acme.
- Save src/provisionTenant.ts.
✔️ Awesome, I've got everything!
Your saved provisioning module is ready for its first API request.
ⓧ I'd like to double check the full code
import "dotenv/config";
import { createClient } from "@tursodatabase/api";
// Authenticate management requests with local environment values.
const turso = createClient({
org: process.env.TURSO_ORG!,
token: process.env.TURSO_PLATFORM_TOKEN!,
});
export async function provisionTenant(customerSlug: string) {
const databaseName = `customer-${customerSlug}`;
// Copy the template so this tenant starts with shared schema and seed data.
return turso.databases.create(databaseName, {
group: "tenants",
seed: {
type: "database",
name: "tenant-template",
},
});
}
// Provision the first customer and surface a concrete success signal.
async function main() {
await provisionTenant("acme");
console.log("Provisioned customer-acme.");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
What should match?
Your file should export provisionTenant with the same group plus seed settings. The final call should pass acme.
Run the provisioner and inspect the copy
Before you run the module, do you think the new customer database starts empty or inherits the template invoice?
- Provision the first tenant by running this command in the open terminal:
npx tsx ./src/provisionTenant.ts
What should I see?
You should see Provisioned customer-acme. in the terminal. That message appears after the Platform API finishes creating the copy.
That is the provisioning loop working. A customer slug now produces a separate database from your template.
Provisioning request failed?
A credential-related failure usually means the token placeholder remains in .env or the organization slug does not match the token scope.
A conflict mentioning the database name means customer-acme already exists. Delete that test database before running this first-customer provisioner again.
Help me diagnose why provisionTenant failed.
- Switch back to the Turso dashboard from earlier.
- Refresh the page to update the database list.
- Select customer-acme from the database list.
- Open the query editor for customer-acme.
Before you query the copy, do you expect to find the template invoice inside it?
- Check the copied invoice by running this query in the editor:
-- Check that the tenant copy includes the template invoice.
SELECT id, customer_name, amount_cents, status
FROM invoices;
What does this query prove?
The query reads the invoice columns from the new customer's own invoices table. A returned template row proves that the API copied both the schema and its seed data.
You should see invoice INV-TEMPLATE-001 for Template Customer. Its amount_cents value is 10000.
Copied invoice missing?
Confirm the selected database is customer-acme. Running the query against another database can hide the copy you just created.
Confirm the seed name in src/provisionTenant.ts is tenant-template.
Help me find why the template invoice was not copied.
Your first customer now has a seeded database of its own. Next up, you will provision another customer and give it different invoice data.
Create a Second Tenant
Your first tenant database proves that provisionTenant can copy the seeded template. A second tenant with different data proves that the boundary holds when customer records diverge.
In this step, you will use the Turso Platform API flow again for globex. You will give its database an invoice that does not appear in customer-acme.
In this step, get ready to:
- Provision customer-globex from the seeded template.
- Add a Globex invoice to the new tenant database.
- Compare both tenant databases to confirm their rows stay separate.
Provision the Globex database
Each call to provisionTenant turns a customer slug into a separate database. Using globex creates customer-globex as a copy of tenant-template.
- Return to the terminal that is already open in the tenant-databases repository.
- Use the same TypeScript run command from the previous step with globex as the customer slug.
The command should finish without an error.
- Switch back to the Turso dashboard from earlier.
- Refresh the database list.
- Select the tenants group.
You should see tenant-template, customer-acme, and customer-globex in the group. That is the second tenant live with its own database.
Add a Globex invoice
A copied database begins with the template schema plus its seeded row. Outerbase lets you add a record through a table editor connected to the selected database.
- Click Open in Outerbase from the customer-globex database page.
- Open the invoices table in the table browser.
- Use the table record controls to add a row.
- Enter INV-GLOBEX-001 in the id field.
- Enter Globex in the customer_name field.
- Enter 25000 in the amount_cents field.
- Enter open in the status field.
- Save the new row with the table record controls.
You should now see two rows in customer-globex. The new row shows INV-GLOBEX-001 with an amount_cents value of 25000 plus an open status.
Why Does Only Globex Change?
Copying tenant-template gave both tenant databases the same schema plus the same starting row. Writing the Globex invoice through customer-globex changes only that database.
Missing the Globex Invoice?
- Confirm that the selected database is customer-globex before adding the row again.
- Check that amount_cents contains 25000 without currency symbols.
- Use the table record controls to save the row before leaving the table.
Help me fix a missing Globex invoice row.
Compare the tenant rows
Before you compare the databases, consider whether the Globex row should also appear in Acme.
- Confirm that the current invoices table contains INV-TEMPLATE-001 plus INV-GLOBEX-001.
- Switch back to the Turso dashboard from earlier.
- Select customer-acme from the tenants group.
- Click Open in Outerbase from the database page.
- Open the invoices table in the table browser.
You will see only the copied INV-TEMPLATE-001 row in customer-acme. The Globex invoice stays inside customer-globex.
You have now proved the tenant boundary with different data. Each customer database can change without changing the other tenant's rows.
Your two tenant databases now hold different invoice rows. Next up, you will run one invoice query against each database to see that isolation from your application.
List Each Tenant's Invoices
Your Turso setup now has two customer databases with different invoice rows. This gives you the data needed to test whether each connection stays inside one customer's database.
The final test uses one SQL query for both customers. The query contains no WHERE clause because the database connection already chooses the tenant.
In this step, get ready to:
- Install the remote database driver.
- Create listInvoices() with a tenant-specific connection.
- Compare the invoice rows returned for Acme and Globex.
Install the remote database driver
The Platform API manages tenant databases. The serverless driver connects to those databases so your application can run queries.
- Add the remote Turso driver to the existing repository by running:
npm install @tursodatabase/serverless
What does this command do?
The command adds @tursodatabase/serverless to the project. This package opens remote database connections with a URL and an authentication token.
npm reports that the package was added. If it was already present, npm reports that the dependencies are current.
- Confirm the installation finishes without a dependency error.
- Keep the terminal in the tenant-databases repository.
Driver installation failed?
Check that the terminal still points to the tenant-databases folder. A network or package registry problem can also interrupt the installation.
Help me diagnose why the Turso serverless package is not installing in my Node.js project.
Write the tenant invoice query
The function turns a customer slug into a database name. It creates a database-scoped read-only token before opening the connection.
The organization slug and Platform API token remain in .env. The generated database token stays in memory instead of entering your source code.
- Switch back to the editor you used for src/provisionTenant.ts.
- Create src/listInvoices.ts inside the existing src folder.
- Add the connection and query function by copying this code into src/listInvoices.ts:
import "dotenv/config";
import { createClient as createPlatformClient } from "@tursodatabase/api";
import { connect } from "@tursodatabase/serverless";
// Reuse the stored Platform API credentials to access tenant databases
const platform = createPlatformClient({
org: process.env.TURSO_ORG!,
token: process.env.TURSO_PLATFORM_TOKEN!,
});
export async function listInvoices(customerSlug: string) {
// Route this request to the database owned by the supplied customer
const databaseName = `customer-${customerSlug}`;
const { jwt } = await platform.databases.createToken(databaseName, {
authorization: "read-only",
});
const connection = connect({
url: `https://${databaseName}-${process.env.TURSO_ORG}.turso.io`,
authToken: jwt,
});
// Read every invoice because the connection already selects the tenant
const statement = await connection.prepare(
"SELECT id, customer_name, amount_cents, status FROM invoices ORDER BY id",
);
return statement.all();
}
What does this code do?
- The databaseName variable maps a slug such as acme to customer-acme.
- The createToken() call creates read-only credentials for that database.
- The connect() call opens a connection to the selected customer's database.
- The SELECT statement reads every invoice visible through that connection.
- Save src/listInvoices.ts.
- Inspect the file for red type-error underlines.
You should see the complete listInvoices() function without unresolved import errors.
Seeing type errors in the function?
Confirm that the import uses @tursodatabase/serverless. Check that TURSO_ORG and TURSO_PLATFORM_TOKEN match the environment entries used by your provisioner.
Help me fix the TypeScript errors in my listInvoices function.
The function now returns rows to its caller. A small command-line runner makes those rows visible in the terminal.
- Place your cursor below the closing brace of listInvoices().
- Add the command-line runner by copying this code:
const customerSlug = process.argv[2];
if (!customerSlug) {
throw new Error("Provide a customer slug.");
}
// Print this tenant's rows in a readable table
listInvoices(customerSlug)
.then((invoices) => console.table(invoices))
.catch((error) => {
console.error(error);
process.exit(1);
});
How does the runner work?
The runner reads the customer slug from the terminal command. It passes that value into listInvoices().
The returned rows appear as a terminal table. A failed connection prints the error and exits the process.
- Save src/listInvoices.ts.
✔️ Awesome, I've got everything!
Your saved file now contains the tenant query and its command-line runner.
ⓧ I'd like to double check the full code
import "dotenv/config";
import { createClient as createPlatformClient } from "@tursodatabase/api";
import { connect } from "@tursodatabase/serverless";
// Reuse the stored Platform API credentials to access tenant databases
const platform = createPlatformClient({
org: process.env.TURSO_ORG!,
token: process.env.TURSO_PLATFORM_TOKEN!,
});
export async function listInvoices(customerSlug: string) {
// Route this request to the database owned by the supplied customer
const databaseName = `customer-${customerSlug}`;
const { jwt } = await platform.databases.createToken(databaseName, {
authorization: "read-only",
});
const connection = connect({
url: `https://${databaseName}-${process.env.TURSO_ORG}.turso.io`,
authToken: jwt,
});
// Read every invoice because the connection already selects the tenant
const statement = await connection.prepare(
"SELECT id, customer_name, amount_cents, status FROM invoices ORDER BY id",
);
return statement.all();
}
const customerSlug = process.argv[2];
if (!customerSlug) {
throw new Error("Provide a customer slug.");
}
// Print this tenant's rows in a readable table
listInvoices(customerSlug)
.then((invoices) => console.table(invoices))
.catch((error) => {
console.error(error);
process.exit(1);
});
Compare Acme with Globex
Both checks execute the same function and the same query. Only the customer slug changes the database connection.
Before you run this, consider whether Acme returns one row or both customers' rows.
- Query Acme's database by running:
npx tsx ./src/listInvoices.ts acme
What does the Acme result prove?
You'll see only INV-TEMPLATE-001 for Template Customer. The Globex invoice does not appear.
The connection to customer-acme limits the query to Acme's database. The query needs no tenant column or filter.
Acme query failed?
Confirm that customer-acme still exists in the tenants group. Check that the stored Platform API token remains valid without displaying its value.
Help me diagnose why listInvoices cannot query customer-acme.
Before you run the next check, consider which extra row should appear when the connection targets Globex.
- Query Globex's database by running:
npx tsx ./src/listInvoices.ts globex
What does the Globex result prove?
You'll see the copied INV-TEMPLATE-001 row. You'll also see INV-GLOBEX-001 for Globex with amount_cents set to 25000.
That difference is your isolation proof. One listInvoices() function returns different rows because each customer has a separate database.
Globex invoice missing?
Return to the customer-globex query view from earlier. Confirm that INV-GLOBEX-001 remains in its invoices table.
Help me find why customer-globex does not return INV-GLOBEX-001.
Secret mission
Audit Tenant Isolation
Prove that your one-database-per-customer design holds up under review. Trace the connection boundary in listInvoices. Compare both customer results to build an isolation evidence set without changing the project.
Clean Up Your Resources
Clean Up Your Resources
No local service needs to stay running, so your computer has no ongoing resource use. The project fits within Turso's Free plan, though the three databases use part of its database allowance until you delete them.
Resources you used:
- Turso database customer-acme in group tenants.
- Turso database customer-globex in group tenants.
- Turso database tenant-template in group tenants.
- Turso database group tenants.
- Local tenant-databases repository containing the TypeScript source files.
- Local .env file containing the stored project configuration.
- Stored Turso Platform API token used by provisionTenant.
Keep everything running
No action is needed. Choose this option if you want to keep building from your database-per-customer example.
- Keep tenant-template as the source for future customer databases.
- Keep customer-acme for repeat isolation checks.
- Keep customer-globex for repeat isolation checks.
- Keep tenant-databases as your starting repository.
- Store .env only on your machine.
- Keep the stored Platform API token private.
Pause - I'll come back to this later
Shut down the local terminal session to pause your work. Turso keeps the databases available for your return.
- Close the open terminal session.
- Leave group tenants in your Turso account.
- Keep the tenant-databases repository on your machine.
- Keep .env out of source control.
Delete - I don't want to use this again
Remove all project resources if you want a complete reset.
Deletion is permanent
Deleted databases lose their invoice rows.
Deleting tenant-databases removes .env. It also removes the source files inside the repository.
- Copy any invoice data you want to keep outside the Turso databases.
- Return to the signed-in Turso dashboard from earlier.
- Select group tenants.
- Select database customer-acme.
- Use its database deletion control to delete customer-acme.
- Select database customer-globex.
- Use its database deletion control to delete customer-globex.
- Select database tenant-template.
- Use its database deletion control to delete tenant-template.
- Use the group deletion control to delete the empty tenants group.
- Refresh the Turso dashboard.
You should no longer see customer-acme, customer-globex, tenant-template, or tenants.
- Return to the Turso dashboard area that lists Platform API tokens.
- Revoke the token stored for tenant-databases.
- Refresh the token list.
The project token should no longer appear as active.
- Choose your operating system below to delete tenant-databases.
macOS
- Move to the parent folder and remove tenant-databases by running these commands:
cd ..
rm -rf tenant-databases
What does this command do?
The first line moves your terminal into the parent folder. The second line removes the repository folder plus everything inside it.
- Verify the removal by listing the remaining items in the parent folder:
ls
What does this check do?
This lists the remaining items in the parent folder. You should not see tenant-databases in the output.
Windows
- Move to the parent folder and remove tenant-databases by running these commands:
cd ..
cmd /c rmdir /s /q tenant-databases
What does this command do?
The first line moves your terminal into the parent folder. The second line removes the repository folder plus everything inside it.
- Verify the removal by listing the remaining items in the parent folder:
dir
What does this check do?
This lists the remaining items in the parent folder. You should not see tenant-databases in the output.
Repository still listed?
- Close any editor that is using a file inside tenant-databases.
- Return to the parent folder before retrying your operating system's removal command.
Help me work out why the tenant-databases folder is still present.
Nice Work!
Nice Work!
You did it! You built a TypeScript service that provisions a separate Turso database for each customer.
You've learned how to:
- Built a repeatable provisioning workflow where provisionTenant copies tenant-template into a new customer database.
- Proved database-level isolation by storing different invoice rows in customer-acme and customer-globex.
- Used one query function through listInvoices to return each customer's invoices without a tenant filter.
- Secret Mission: Extended the tenant database pattern through an optional challenge that pushed your isolation skills further.
Ready to quiz yourself?