Branch a Database and Its Files
Branch a Neon database and its files, then prove each branch stays isolated.
Introduction
30 Second Summary
A copied workspace can look safely separated while every copy still reaches the same uploaded files. One test deletion can then damage the original.
In this project, you will build a Neon project where every database row points to a matching file in object storage. A Node.js function writes each pair before database branching separates them for a delete-and-reset test.
What You'll Build
You see two bucket listings side by side, with the branch missing a file that remains safely on main.
By the end of this project, you'll have:
- Queryable rows and file keys that appear together in the SQL editor. Each key matches a file in the bucket.
- A one-call write that creates a row plus its matching file. The function log confirms the write.
- A side-by-side branch proof showing one file missing from the branch while main keeps its copy. A reset restores the file plus its row.
- Secret Mission: An optional challenge to push your skills further.
Are there any prerequisites?
You'll need a Neon account plus Node available on your computer. Basic SQL familiarity helps with the table and row checks.
Before We Start
This project compares the same rows plus files across two database branches. A signed-in Neon Console gives that comparison one controlled home.
Later, a Node.js function creates a row plus its matching file in one request. This step confirms that your local runtime can support that function.
In this step, get ready to:
- Access the Neon Console with your account.
- Confirm your local Node.js toolchain is available.
- Keep the build on Neon Database plus Neon Object Storage.
Sign in to the Neon Console
Your Neon account controls access to every resource you create later. Authentication stays inside Neon's sign-in flow, so this project never asks you to paste a password or token.
This setup uses a free Neon account. No paid plan is needed.
Choose the tab that matches your current account status.
✔️ I already have an account
- Open the Neon Console sign-in page in your browser.
- Complete the authentication prompts using your Neon account.
- Confirm that the signed-in Neon Console loads.
You're signed in. The Neon Console is ready to hold the resources you create in the next step.
ⓧ I need an account
- Open the Neon account sign-up page in your browser.
- Complete the sign-up prompts to create your account.
- Open the Neon Console sign-in page after creating your account.
- Complete the authentication prompts using your new account.
- Confirm that the signed-in Neon Console loads.
Your account is ready. The Neon Console can now hold the resources you create in the next step.
Keep the build on the free path
The planned usage fits within Neon's free plan at this project's size.
This build uses Neon Database for rows. It uses Neon Object Storage for matching files.
AI Gateway uses pay-as-you-go billing. It stays outside this project.
Having trouble signing in?
- Return to the Neon Console sign-in page if the authentication window closes.
- Use the same authentication method associated with your Neon account.
Still stuck? Help me troubleshoot access to my Neon account. You can also ask the Neon Discord community.
Verify the local runtime
Node.js runs the function you deploy later. The npm package manager supplies the function's dependencies.
- Press Command-Space bar on macOS or Windows key + S on Windows to open system search.
- Type Terminal on macOS or PowerShell on Windows into the search field.
- Press Return on macOS or Enter on Windows to open the terminal.
Before you run the check, do you expect your terminal to recognize both tools?
- Check whether Node.js plus npm are available by running:
node -v
npm -v
What do these commands check?
- The node command uses -v to print the installed Node.js version.
- The npm command uses -v to print the installed package manager version.
- Two version strings confirm that both tools are available in this terminal.
Choose the tab that matches your terminal output.
✔️ I see two version numbers
That's the local setup done. Your terminal can run Node.js plus npm when you deploy the function later.
ⓧ One or both commands fail
The official Node.js installer includes npm. Installing the LTS release prepares both tools together.
Installing software can feel intrusive. The official installer only adds the runtime plus its package manager to your computer.
- Open the official Node.js download page in your browser.
- Download the installer labeled LTS for your operating system.
- Run the installer you downloaded.
- Approve the operating system permission request if one appears.
- Complete the installer prompts using the default options.
- Close your current terminal window.
- Open a fresh terminal using the same search steps from above.
- Check the installation by running:
node -v
npm -v
What confirms the installation?
The fresh terminal loads the updated executable path. You should now see one version string for Node.js.
You should also see one version string for npm. Those results confirm that the toolchain is ready.
Still missing a version number?
- Close every terminal window on your computer.
- Open a fresh terminal using system search.
- Run the Node.js installer again if the npm check still fails.
Still stuck? Help me troubleshoot my Node.js and npm installation. You can also compare your setup with the official npm installation guide.
Your browser should now show the signed-in Neon Console. Your terminal should show one Node.js version plus one npm version.
Your setup is ready. Next, you'll create the Neon project that keeps database rows tied to their files.
Create the Database Table
Each database row needs a predictable file key before its matching file exists. That shared key gives you something concrete to compare after branching.
In this step, you'll create a free Neon project with a default branch named main.
You'll create the table that connects each row to a file key. Two starter rows give the next step exact keys for its files.
In this step, get ready to:
- Create the free Neon project with main as its default branch.
- Create the file_records table on main.
- Store two matching file keys in the table.
Create the project and prepare its branch
A Neon project is the container for this database. Projects created in the Console begin with a root branch named production.
Your free plan covers this small database project. Keeping optional services disabled leaves this step focused on the database.
- In the Neon Console from earlier, click New Project.
- Enter branch-files-together in the Project name field.
- Choose the Region closest to you.
- Keep Postgres database enabled under Services.
- Leave every optional service disabled for now.
- Click Create project.
You'll land on the Project Dashboard. That is your cloud workspace created with the database ready.
- Select Branches in the left sidebar.
- Select the production branch from the table.
- Open the three-dot menu on the branch overview page.
- Select Rename.
- Enter main as the new branch name.
- Click Save.
You'll see main with a DEFAULT tag. Your root branch now has the name used throughout this project.
Project or branch not showing up?
- Confirm the project selector shows branch-files-together.
- Refresh the Branches page if the rename is still processing.
- Confirm you clicked Save after entering main.
Still stuck? Help me troubleshoot my Neon project or branch rename. You can also ask the Neon community.
Create the file records table
The Postgres database runs each SQL statement against the branch selected in the editor. Selecting main keeps this starting table on the branch that later tests must preserve.
- Select Postgres database in the left sidebar.
- Select SQL Editor.
- Select main from the branch selector.
- Keep the current database selected.
- Replace any placeholder query in the editor with the SQL below:
-- Give each record an automatic ID
CREATE TABLE file_records (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
file_key TEXT NOT NULL
);
What does this table do?
- The id column assigns a stable number to every record.
- The GENERATED ALWAYS AS IDENTITY rule lets Postgres create each number automatically.
- The file_key column stores the path used by the matching object storage file.
- The NOT NULL rule prevents a row from missing its file key.
- Click Run.
You'll see the editor report a successful query. That's the database foundation in place: main now contains the file_records table.
Table query not succeeding?
- Confirm the branch selector still shows main.
- Check that both column definitions sit inside the parentheses.
- Check that the statement ends with a semicolon.
- Still stuck? Help me fix my file_records table query.
Insert and query the starter rows
Each stored key must match a future file name character for character. These two rows establish that shared naming contract before any files are uploaded.
- Replace the table query with the insert statement below:
-- Seed the exact keys that the files will use
INSERT INTO file_records (file_key)
VALUES
('files/first-file.txt'),
('files/second-file.txt');
What does this insert do?
- The statement creates one record for each future file.
- The files/ prefix keeps both objects under the same logical path.
- Leaving id out of the insert lets Postgres generate the identifiers.
- Click Run.
You'll see the editor report a successful insert. Good work. The database now holds the two keys that the object storage files must match.
Insert query failing?
- Confirm the table name is file_records.
- Check that each file key uses single quotes.
- Check that the two values are separated by a comma.
- Still stuck? Help me fix my starter row insert.
Before you run the final check, which two file keys do you expect the query results to show?
- Replace the insert statement with the verification query below:
-- Show both stored keys in a stable order
SELECT id, file_key
FROM file_records
ORDER BY id;
What does this query check?
- The SELECT statement returns each generated identifier with its matching file key.
- The ORDER BY id clause keeps the result in insertion order.
- Click Run.
You'll see two rows. Row 1 shows files/first-file.txt.
Row 2 shows files/second-file.txt. This proves the table on main contains both required keys.
Rows missing from the results?
- Confirm the branch selector shows main.
- Run the insert statement again if the earlier insert did not report success.
- Check that the query reads from file_records.
- Still stuck? Help me find my missing file records.
✔️ Awesome, I've got everything!
Your main branch contains the file_records table with both required file keys.
ⓧ I'd like to double check the full code
This is the complete SQL used to create the table. It also seeds and verifies the two rows.
-- Give each record an automatic ID
CREATE TABLE file_records (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
file_key TEXT NOT NULL
);
-- Seed the exact keys that the files will use
INSERT INTO file_records (file_key)
VALUES
('files/first-file.txt'),
('files/second-file.txt');
-- Show both stored keys in a stable order
SELECT id, file_key
FROM file_records
ORDER BY id;
Your main branch now holds two rows with stable file keys. Next up, you'll create the matching objects and prove that every row points to a file.
Add Matching Object Files
Your file_records table now gives each row a file_key. Those values still point to empty addresses, so a later branch comparison would prove nothing about files.
Neon Object Storage gives those keys real objects on the same branch. This step creates one bucket with two matching objects.
In this step, get ready to:
- Create a branch-scoped object storage bucket.
- Upload one object for each database row.
- Compare the object keys with the stored file keys.
Create the branch-scoped bucket
A bucket is a named container for stored objects. Neon scopes each bucket to the selected branch, which prepares the files to follow the same branching workflow as your rows.
- Confirm branch-files-together remains selected in the project switcher.
- Confirm main remains selected in the branch selector.
- Select Object storage in the project navigation.
- Click New bucket.
- Enter branch-files-bucket in the bucket name field.
- Keep the access level set to private.
- Click Create.
You will see branch-files-bucket in the bucket list for main. That is the storage container ready for both matching objects.
Why does the branch matter?
A child branch inherits the buckets and objects held by its parent at fork time. Later object changes stay isolated to the branch where they happen.
Bucket not appearing?
- Confirm the selected project is branch-files-together.
- Confirm the selected branch is main.
- Check that the bucket name is exactly branch-files-bucket.
- Still stuck? Help me troubleshoot why my Neon Object Storage bucket is missing.
Upload the first matching object
An object key is the path that identifies a file inside a bucket. The complete key must match the database value character for character.
- Open branch-files-bucket from the bucket list.
- Select the bucket-page control for uploading a new object.
- Choose any small local text file in the file picker.
- Set the destination object key to files/first-file.txt.
- Complete the upload.
- Refresh the bucket object list.
You will see an object with the key files/first-file.txt. The first database row now points to a file that exists.
Keep the destination key exact
The local filename only supplies the file body. The destination object key supplies the identity stored in file_records.
First object key looks different?
- Check that the destination key includes the files/ prefix.
- Check that the filename ends with first-file.txt.
- Retry the upload with files/first-file.txt as the complete destination key.
- Still stuck? Help me fix the object key for my first upload.
Upload the second matching object
The second row needs its own exact object key. You can reuse the same local text file because this check focuses on the destination key.
- Select the bucket-page control for uploading another object.
- Choose the small local text file in the file picker.
- Set the destination object key to files/second-file.txt.
- Complete the upload.
Before you refresh the object list, do you expect both database keys to appear as complete paths or as bare filenames?
- Refresh the bucket object list.
- Expand the files prefix if the bucket browser groups keys into folders.
- Compare the listed object keys with the two file_key values from the SQL Editor earlier.
You will see files/first-file.txt and files/second-file.txt in branch-files-bucket. Both paths exactly match the database rows.
You have locked in the pairing. Each database row now names an object that exists on main.
Two matching keys not listed?
- Open the files prefix if the bucket browser shows it as a folder.
- Check both object names for missing hyphens.
- Check both object names for missing .txt extensions.
- Still stuck? Help me compare my bucket objects with my database file keys.
Your rows and files now share the same keys on the main branch. Next, you will deploy a function that creates this pairing in one call.
Deploy the File Writer Function
Your main branch already has two database rows whose file keys match two stored objects. Creating both pieces manually works for a small test, but an application needs one request to coordinate them.
In this step, you will deploy a Neon Function that runs Node.js next to your Postgres database. One invocation inserts a row into file_records and uploads the matching object to Neon Object Storage.
In this step, get ready to:
- Link a local function project to the main Neon branch.
- Define the database client and storage client.
- Deploy the function and verify its database row, object, and log entry.
Set up the local function project
The Neon CLI stores project context in the folder where you run it. That context ensures every deployment in this step targets the existing branch-files-together project on its main branch.
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The command moves your terminal into the Desktop folder. Your local function files will be easy to find there.
- Create the branch-files-function folder and initialize its Node.js package by running:
mkdir branch-files-function && cd branch-files-function && npm init -y
What does this command do?
- The first command creates the branch-files-function folder on your Desktop.
- The second command moves your terminal into that folder.
- The final command creates package.json for the function dependencies.
You will see a new package.json summary in the terminal. Your local function project now has a package manifest.
- Install the latest Neon CLI by running:
npm install -g neon@latest
What does this command do?
The command installs the neon executable for your whole computer. You will use it to link, deploy, inspect, and invoke the function.
Having trouble installing the Neon CLI?
- Check that the install output finishes without a permissions or network failure.
- Confirm that your terminal can still run npm after the installation.
- Need another pair of eyes? Help me troubleshoot my Neon CLI installation. You can also ask in the Neon Discord community.
Linking can open a browser authorization screen. Keep your existing signed-in Neon Console session available so the CLI can connect without exposing credentials in your terminal.
- Choose the existing branch-files-together project when the CLI asks which project to link.
- Choose the existing main branch when the CLI asks which branch to pin.
- Decline the optional configuration-file prompt because you will add the project configuration in the next substep.
- Start the interactive link process by running:
neon link
What does linking create?
The CLI writes a .neon context file that identifies the selected project and branch. It also pulls branch variables such as DATABASE_URL into a local environment file.
That connection is now locked in. Commands from this folder target the same main branch that holds your first two rows and objects.
- Install the function, database, storage, and TypeScript dependencies by running:
npm install @neon/config @neon/functions pg @aws-sdk/client-s3
npm install --save-dev @types/pg
What do these packages provide?
- The @neon/config package defines which function Neon deploys.
- The @neon/functions package keeps the long-running database pool healthy.
- The pg package connects the function to Postgres.
- The @aws-sdk/client-s3 package uploads an object through Neon's S3-compatible endpoint.
Dependency installation failed?
- Confirm that your terminal is inside ~/Desktop/branch-files-function.
- Check the terminal output for the package name that failed to download.
- Still stuck? Help me fix my function dependency installation.
Define and deploy the function
A function configuration separates its permanent CLI slug from its display name. The split is easy to miss, so you will use the short slug writefile while the Neon Console shows the required name write-record-and-file.
- Open a new neon.ts file in the branch-files-function folder by running:
nano neon.ts
What does this command do?
The command opens neon.ts in the terminal editor. Nano creates the file when it does not already exist.
- Paste this function configuration into neon.ts:
import { defineConfig } from '@neon/config/v1';
// Declare the function Neon should deploy on the linked branch.
export default defineConfig({
functions: {
writefile: {
name: 'write-record-and-file',
source: './functions/write-record-and-file.ts',
},
},
});
What does this configuration do?
- The writefile key becomes the permanent slug used by CLI commands.
- The name value becomes the display name shown for the deployed function.
- The source value points Neon at the request handler you will create shortly.
- Save neon.ts by pressing Ctrl+O.
- Confirm the filename by pressing Enter.
- Exit Nano by pressing Ctrl+X.
- Confirm the configuration file exists by running:
ls neon.ts
What does this check prove?
The command prints neon.ts when the file exists in the current folder. This confirms the deployment configuration was saved in the expected location.
Configuration file not listed?
- Run the Nano command again from ~/Desktop/branch-files-function.
- Press Ctrl+O before exiting so Nano writes the file.
- Still stuck? Help me save my Neon function configuration.
✔️ Awesome, I've got everything!
Your neon.ts file now declares the write-record-and-file function.
ⓧ I'd like to double check the full code
Compare your complete neon.ts file with this reference.
import { defineConfig } from '@neon/config/v1';
// Declare the function Neon should deploy on the linked branch.
export default defineConfig({
functions: {
writefile: {
name: 'write-record-and-file',
source: './functions/write-record-and-file.ts',
},
},
});
The function needs reusable clients for both services. A module-level database pool and storage client can stay available across multiple requests.
- Create the functions folder and open functions/clients.ts in Nano by running:
mkdir -p functions && nano functions/clients.ts
What does this command do?
The first command creates the functions folder when needed. The second command opens the client module inside that folder.
- Paste this client setup into functions/clients.ts:
import { S3Client } from '@aws-sdk/client-s3';
import { attachDatabasePool } from '@neon/functions';
import { Pool } from 'pg';
// Keep one database pool alive across function invocations.
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5,
});
// Recover cleanly from expected idle database disconnects.
attachDatabasePool(pool);
// Point the standard S3 client at this branch's storage endpoint.
const storage = new S3Client({
region: process.env.AWS_REGION,
endpoint: process.env.AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
});
// Reuse the bucket that already holds the first two files.
const bucket = 'branch-files-bucket';
export { bucket, pool, storage };
What does this client module do?
- The Pool reads the branch-scoped DATABASE_URL that Neon injects at runtime.
- The S3Client reads the branch-scoped storage endpoint and credentials that Neon injects.
- The forcePathStyle setting makes the AWS SDK use Neon's supported path-style addressing.
- The bucket constant directs uploads to branch-files-bucket.
- Save functions/clients.ts by pressing Ctrl+O.
- Confirm the filename by pressing Enter.
- Exit Nano by pressing Ctrl+X.
- Confirm the client module exists by running:
ls functions/clients.ts
What does this check prove?
The command prints the complete path when the client module was saved inside functions.
Client module not listed?
- Check that the folder name is exactly functions.
- Check that the filename is exactly clients.ts.
- Still stuck? Help me create the Neon function client module.
✔️ Awesome, I've got everything!
Your shared database and storage clients are saved in functions/clients.ts.
ⓧ I'd like to double check the full code
Compare your complete functions/clients.ts file with this reference.
import { S3Client } from '@aws-sdk/client-s3';
import { attachDatabasePool } from '@neon/functions';
import { Pool } from 'pg';
// Keep one database pool alive across function invocations.
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5,
});
// Recover cleanly from expected idle database disconnects.
attachDatabasePool(pool);
// Point the standard S3 client at this branch's storage endpoint.
const storage = new S3Client({
region: process.env.AWS_REGION,
endpoint: process.env.AWS_ENDPOINT_URL_S3,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
});
// Reuse the bucket that already holds the first two files.
const bucket = 'branch-files-bucket';
export { bucket, pool, storage };
The request handler generates one file key for both systems. Reusing the same value prevents the database row from pointing at a different object name.
- Open functions/write-record-and-file.ts in Nano by running:
nano functions/write-record-and-file.ts
What does this command do?
The command opens the handler file at the source path declared in neon.ts.
- Paste this request handler into functions/write-record-and-file.ts:
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { randomUUID } from 'node:crypto';
import { bucket, pool, storage } from './clients';
export default {
async fetch() {
// Generate one key that the row and object can share.
const fileKey = `files/function-${randomUUID()}.txt`;
const fileBody = `Created by write-record-and-file at ${new Date().toISOString()}.`;
// Store the generated key in the database row.
await pool.query(
'INSERT INTO file_records (file_key) VALUES ($1)',
[fileKey],
);
// Upload the file under the exact same key.
await storage.send(new PutObjectCommand({
Bucket: bucket,
Key: fileKey,
Body: fileBody,
ContentType: 'text/plain',
}));
// Leave a log line that ties both writes to one key.
console.log(`Created row and file: ${fileKey}`);
return Response.json({ fileKey });
},
};
What does this handler do?
- The randomUUID() call generates a unique filename for this invocation.
- The parameterized query inserts that key into the file_records table.
- The PutObjectCommand uploads a text object under the same key.
- The response and log entry expose the generated key so you can compare all three results.
- Save functions/write-record-and-file.ts by pressing Ctrl+O.
- Confirm the filename by pressing Enter.
- Exit Nano by pressing Ctrl+X.
- Confirm both function files exist by running:
ls functions
What does this check prove?
The output should list clients.ts and write-record-and-file.ts. Neon can now resolve the configured source file and its imported client module.
One of the function files is missing?
- Check that both files are inside the same functions folder.
- Check that the handler filename includes every hyphen in write-record-and-file.ts.
- Still stuck? Help me correct my Neon function file structure.
✔️ Awesome, I've got everything!
Your handler now inserts one row and uploads its matching object during the same request.
ⓧ I'd like to double check the full code
Compare your complete functions/write-record-and-file.ts file with this reference.
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { randomUUID } from 'node:crypto';
import { bucket, pool, storage } from './clients';
export default {
async fetch() {
// Generate one key that the row and object can share.
const fileKey = `files/function-${randomUUID()}.txt`;
const fileBody = `Created by write-record-and-file at ${new Date().toISOString()}.`;
// Store the generated key in the database row.
await pool.query(
'INSERT INTO file_records (file_key) VALUES ($1)',
[fileKey],
);
// Upload the file under the exact same key.
await storage.send(new PutObjectCommand({
Bucket: bucket,
Key: fileKey,
Body: fileBody,
ContentType: 'text/plain',
}));
// Leave a log line that ties both writes to one key.
console.log(`Created row and file: ${fileKey}`);
return Response.json({ fileKey });
},
};
Neon injects the main branch database connection and storage credentials into the deployed runtime. You do not need to copy those credentials into your source files.
The CLI waits while it bundles and deploys the function. Allow up to ten minutes for its deployment wait window, so a quiet terminal during the build does not mean it has stalled.
- Deploy the function configuration to the linked main branch by running:
neon deploy
What does this deployment do?
The CLI reads neon.ts and bundles the configured TypeScript source. It uploads the bundle as the write-record-and-file function on the linked branch.
- Inspect the deployed function by running:
neon functions get writefile
What should you inspect?
The function details include its deployment status and public URL. A completed status confirms that the build is live.
You should see the display name write-record-and-file with a completed deployment. The details also show the URL you will invoke once.
- Record the displayed function URL here: your function invocation URL.
Deployment did not complete?
- Check that neon.ts points to ./functions/write-record-and-file.ts.
- Check that every imported package appears in package.json.
- Still stuck? Help me diagnose my Neon Function deployment.
Invoke the function and verify both writes
The deployment exists, but it has not created a third record yet. One HTTP request now exercises the database insert, object upload, response, and log entry.
Before you call the function, do you expect one shared file key or separate identifiers from the two services?
- Invoke the deployed function once by running:
curl [[FUNCTION_URL="your function invocation URL"]]
What does this request trigger?
The request runs the deployed handler once. The handler generates one key before sending that same value to Postgres and Object Storage.
You will receive a JSON response containing fileKey. That value identifies the new row and the new object.
- Record the returned fileKey here: your generated file key.
- Read the recent function logs by running:
neon logs query --source function --since 1h --body-contains "Created row and file:"
What does this log query show?
The query limits results to function logs from the last hour. The body filter selects the line written after the database insert and object upload finish.
You should see a line beginning Created row and file: followed by your generated file key. The shared key proves that the invocation used one identifier for both writes.
No matching function log?
- Check that the HTTP request returned a fileKey value.
- Check that the linked branch is still main.
- Still stuck? Help me find my Neon Function log entry.
- Switch back to Neon Object Storage from earlier.
- Refresh the object list for branch-files-bucket.
You should see the original two objects plus a third object named your generated file key.
- Return to the SQL Editor from earlier.
Before you query the table, how many file keys do you expect to find now?
- Query every stored file key by running:
-- Confirm that the function added one matching database row.
SELECT id, file_key
FROM file_records
ORDER BY id;
What does this query confirm?
The query returns every identifier and file key in insertion order. The final row should carry the same key shown in the function response, log, and bucket.
You will see three rows. The newest file_key matches your generated file key.
Row or object missing?
- Compare the key in the HTTP response with the key in the function log.
- Confirm that the SQL Editor is still using the main branch.
- Confirm that Object Storage is showing branch-files-bucket on the main branch.
- Still stuck? Help me trace the missing function write.
That is the full write path working. Your function now creates a database row, uploads its matching file, and leaves one key you can trace across both systems. Next, you will branch the project and prove that deleting this data on the branch leaves main untouched.
Break the Branch Copy
Your deployed function now creates a database row plus its matching object in one invocation. That paired state is ready for a branch test.
An isolated database branch only solves half the problem when its rows point to shared files. This step tests whether Neon Object Storage follows the same boundary as Postgres.
In this step, get ready to:
- Create file-delete-test from the current state of main.
- Remove the row-object pair for files/first-file.txt from the child branch.
- Compare the child branch with main.
Create the child branch
A copy-on-write branch starts with the current state of its parent. Later changes stay scoped to the child branch.
- Switch back to the Neon Console from earlier.
- Select Branches in the sidebar.
- Click New branch.
- Set Parent branch to main.
- Select Current data.
- Enter file-delete-test as the branch name.
- Click Create.
Good progress. file-delete-test now gives you a child branch for the deletion test.
- Select Postgres database in the sidebar.
- Select SQL Editor.
- Select file-delete-test in the branch selector.
- Replace the editor contents with this query:
SELECT id, file_key
FROM file_records
ORDER BY id;
What Does This Query Check?
- The query reads every row from file_records.
- Each result shows the row ID beside its object key.
- The ID order makes later comparisons easier to scan.
- Click Run to query the child branch.
You should see three rows. The results include files/first-file.txt plus files/second-file.txt.
The third row should contain the file key created by write-record-and-file.
Missing Rows on the Child Branch?
- Check that the SQL Editor branch selector shows file-delete-test.
- Confirm that main is the parent of the new branch.
- Still stuck? Help me check the rows inherited by my Neon branch.
- Return to Object storage from earlier.
- Select file-delete-test in the project branch selector.
- Open branch-files-bucket.
- Open the files/ prefix.
You should see first-file.txt plus second-file.txt. You should also see the function-created object.
This confirms that the branch inherited the storage state before your deletion.
Remove the branch pair
Deleting a row plus its file can feel risky. The active file-delete-test branch contains this experiment away from main.
- Return to the SQL Editor from earlier.
- Check that the branch selector shows file-delete-test.
- Replace the current query with this deletion:
DELETE FROM file_records
WHERE file_key = 'files/first-file.txt'
RETURNING id, file_key;
What Does This Deletion Do?
- The WHERE condition targets the row linked to files/first-file.txt.
- The RETURNING clause displays the deleted row.
- The other rows remain in file_records.
Before you run the deletion, which branch do you expect to lose the row?
- Click Run to delete the row from the selected branch.
You should see one returned row with the key files/first-file.txt. This proves that the child branch lost the target row.
No Row Returned?
- Check that the query contains the exact key files/first-file.txt.
- Verify that file-delete-test remains selected.
- Still stuck? Help me troubleshoot the row deletion on my child branch.
- Return to Object storage from earlier.
- Check that the project branch selector shows file-delete-test.
- Open branch-files-bucket.
- Open the files/ prefix.
- Find first-file.txt in the object listing.
- Open the object controls beside first-file.txt.
- Choose the option that removes the object.
- Confirm the object deletion when the Console asks.
You should now see second-file.txt plus the function-created object. The first-file.txt object should be absent.
The child branch now lacks the same pair in its database and storage views.
Object Still Listed?
- Check that the project branch selector shows file-delete-test.
- Refresh the bucket listing after completing the deletion.
- Still stuck? Help me remove the object from the child branch only.
Compare branch and main
The strongest proof is a visible disagreement between the two object listings. Keeping two branch views straight is a bit fiddly.
Use the branch name in each window as your anchor.
- Copy the current page URL.
- Create a second browser window.
- Paste the copied URL into the second window's address bar.
- Place the file-delete-test window on the left side of your screen.
- Place the second window on the right side of your screen.
- Select main in the project branch selector on the right.
- Open branch-files-bucket in the right window.
- Open the files/ prefix in the right window.
Before you refresh the listings, do you expect first-file.txt to be missing from one branch or both?
- Refresh the file-delete-test listing on the left.
- Refresh the main listing on the right.
The left listing should lack first-file.txt. The right listing should still contain it.
There is your proof. The child branch accepted a real file deletion without changing the parent copy.
- Return to SQL Editor in the right window.
- Check that the branch selector shows main.
- Replace the editor contents with this query:
SELECT id, file_key
FROM file_records
ORDER BY id;
What Does This Final Query Prove?
This query reads the parent table after the child deletion. Its results reveal whether the matching row survived on main.
Before you run the query, do you expect files/first-file.txt to appear in the parent results?
- Click Run to query main.
You should see all three parent rows. The results should include files/first-file.txt plus files/second-file.txt.
You should also see the key created by write-record-and-file.
Missing the Parent Row?
- Check that the right SQL Editor branch selector shows main.
- Confirm that the deletion query ran while file-delete-test was selected.
- Still stuck? Help me compare the parent row with the child branch state.
- Return to the left Neon Console window with file-delete-test selected.
Your branch now differs from main across both rows and files. Next, you will reset the child branch to test whether both pieces return together.
Reset the Branch from Main
You proved that a deletion on file-delete-test leaves main untouched. The test branch still carries the deletion.
Now you'll use a Neon branch reset to recover the pair. The reset should restore the Postgres row from main. It should restore the Object Storage object at the same moment.
In this step, get ready to:
- Reset file-delete-test from its parent branch.
- Confirm the deleted database row returned.
- Verify the bucket matches main again.
Reset the branch from its parent
A parent reset replaces the child branch's current state with the latest state from its parent. This discards the branch-only changes in one operation.
Replacing a branch can feel risky. Here, file-delete-test contains only the test deletions you planned to discard.
- Return to the Branches page from the current branch details.
- Select file-delete-test from the branch list.
You should see main identified as the parent of file-delete-test.
The reset action opens a confirmation prompt before Neon replaces the branch state.
Before you reset, picture whether the deleted row and object will return together.
- Open the three-dot menu on the branch details page.
- Select Reset from parent.
The confirmation prompt should identify main as the source for the reset.
- Verify that main is identified as the parent.
- Approve the reset using the confirmation control.
Neon now starts replacing the test branch state with the current state from main.
- Wait for the reset operation to finish.
That is the risky part complete. The branch details now show that file-delete-test was reset from its parent.
What did the reset change?
The reset aligns the branch's database state with main. It also aligns the branch's bucket state with the same parent.
The deployed write-record-and-file function remains configured for the project.
Reset option unavailable?
- Check that file-delete-test is selected because a root branch cannot reset from a parent.
- Return to the branch details page if the Reset from parent action is missing from the current view.
- Still blocked? Help me reset my Neon child branch from main..
Confirm the database row returned
The missing row was a branch-only deletion. The reset should rebuild the current file_records table from main.
- Select Postgres database in the Neon Console sidebar.
- Select Tables.
You should see the table explorer for the currently selected project.
Before you inspect the table, predict which missing file key should return.
- Choose file-delete-test in the branch selector.
- Select the file_records table.
You should see rows for files/first-file.txt and files/second-file.txt. You should also see the row added by write-record-and-file.
The deleted row is back in the test branch. Its return confirms that the database state now matches main.
Row still missing?
- Check that the branch selector shows file-delete-test.
- Reload the table after the branch reset finishes.
- Still missing the row? Help me verify the reset database state..
Verify the bucket matches main
The database check proves that the row returned. The bucket listing now reveals whether its matching file returned in the same reset.
- Select Object storage in the Neon Console.
- Choose file-delete-test in the branch selector.
You should see the buckets available on the reset branch.
Before you inspect the bucket, predict whether the missing object key returned with its row.
- Select branch-files-bucket to load its object listing.
- Open the files/ folder if the console groups object keys by prefix.
You should see files/first-file.txt and files/second-file.txt. You should also see the object created by write-record-and-file.
You now have the complete proof. The row and its matching file returned together from main.
Object still missing?
- Check that the Object Storage branch selector shows file-delete-test.
- Reload the branch-files-bucket object listing after the reset finishes.
- Still missing the object? Help me verify the reset bucket state..
You have proved the project's core claim: one reset restored the file and its database record together. The main branch stayed unchanged throughout.
Secret mission
Audit Row-to-Object Consistency
Audit both branches for row-to-object consistency without changing their data. You will pair each database key with its stored object and capture evidence another engineer can review.
Clean Up Your Resources
Clean Up Your Resources
At its current size, your Neon project stays within the free plan. Decide whether to keep the reset branch available, pause your testing, or delete only the temporary branch while keeping main.
Resources you used:
- The temporary file-delete-test branch in the branch-files-together project. It contains branched copies of the file_records table and the objects in branch-files-bucket.
The main branch keeps the completed file_records table and branch-files-bucket. The deployed write-record-and-file function also stays with the project.
Keep everything running
No cleanup action is needed while you are still testing the branching workflow. Both branches remain ready for more comparisons.
- Keep file-delete-test available for more comparisons with main.
- Keep write-record-and-file deployed for more row and file creation tests.
- Continue avoiding AI Gateway to keep this build on the free path.
Pause - I'll come back to this later
A pause preserves the reset state for your next session. No local process needs to be stopped for this project.
- Stop invoking write-record-and-file until you return.
- Leave file-delete-test in its current reset state.
- Keep main unchanged as your completed branching proof.
Delete - I don't want to use this again
Deleting the temporary branch removes its branched rows and objects. Your main branch remains untouched.
Neon asks you to confirm the branch deletion before it proceeds.
- Switch back to main in the Neon Console for branch-files-together.
- Open the project's branch list.
- Open the actions menu for file-delete-test.
- Choose the branch deletion option.
- Confirm the deletion.
- Refresh the branch list.
You should no longer see file-delete-test. The main branch should still be available.
- Open file_records on main to confirm all three records remain.
- Open branch-files-bucket on main to confirm all three objects remain.
That's a clean finish. Your completed project still holds the matching rows and objects that prove database branches can include files.
Nice Work!
Nice Work!
Strong finish. Your branch-files-together project proves that Neon can branch database rows together with their matching files.
You've learned how to:
- Keep database rows aligned with objects in Neon Object Storage through matching file keys.
- Deploy a Node.js function that creates a file_records row plus its matching object in a single call. Read its log to confirm the created file key.
- Prove branch isolation by removing a row plus its referenced object from file-delete-test while main stays unchanged. Use a branch reset to restore both from main.
- Extend your branch-aware file workflow through the optional Secret Mission.
Ready to quiz yourself?