When deploying your Sequelize applications to Render on the free tier, you only get one database instance. Think of this like having one big house for all your projects. You still need to keep each project’s belongings (tables) separate, and you can do that by using PostgreSQL “schemas.” This separation avoids conflicts between different applications trying to use similarly named tables.
In this tutorial, you’ll learn how to modify your migration and seeder files so that each of your projects uses its own schema in that single Render database. By doing this, you tell Postgres which “room” (schema) your table or data belongs to.
Imagine you have one library (the Render database) but multiple sections (schemas)—each “section” holds a different project’s books (tables). This keeps one project’s tables from stepping on another’s. So if you’re building multiple Express apps, each one can have its own “room” within the same database.
options Object
To direct Sequelize to the correct schema in production,
you’ll use an options object that includes schema
if NODE_ENV === 'production'.
This snippet belongs at the top of every migration and seeder file
(but outside your up and down functions):
let options = {};
if (process.env.NODE_ENV === 'production') {
options.schema = process.env.SCHEMA; // define your schema in options object
}
When NODE_ENV is production,
options.schema points Sequelize to your custom schema name
(taken from the .env variable SCHEMA).
This is how each project can live in its own subsection within the Render database.
createTable
For migrations that create a table,
pass the options object as the third argument to queryInterface.createTable.
For example:
return queryInterface.createTable("Users", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
username: {
type: Sequelize.STRING(30),
allowNull: false,
unique: true
},
// ...
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, options); // pass options as 3rd argument
Remember, options might look like:
let options = {};
if (process.env.NODE_ENV === 'production') {
options.schema = process.env.SCHEMA;
}
queryInterface methods
For all other queryInterface methods, you pass options
as the first argument, but you need to include
options.tableName = "TableName".
This replaces the usual string argument that indicates the table.
Here’s an example using addColumn:
'use strict';
let options = {};
if (process.env.NODE_ENV === 'production') {
options.schema = process.env.SCHEMA; // define your schema
}
module.exports = {
async up (queryInterface, Sequelize) {
options.tableName = "Users"; // specify the table name
await queryInterface.addColumn(options, 'firstName', {
type: Sequelize.STRING(30)
});
},
async down (queryInterface, Sequelize) {
options.tableName = "Users"; // specify the same table name
await queryInterface.removeColumn(options, 'firstName');
}
};
Notice how addColumn (and similarly removeColumn,
changeColumn, renameColumn) doesn’t take the table name
as its second argument, but rather you pass it in the options object
as options.tableName. This is crucial for telling Postgres
which table (and schema) you want to modify.
Seeder files also need the schema in production. Typically, you’ll place the same snippet at the top:
'use strict';
const bcrypt = require("bcryptjs");
let options = {};
if (process.env.NODE_ENV === 'production') {
options.schema = process.env.SCHEMA; // define your schema in options object
}
module.exports = {
async up (queryInterface, Sequelize) {
options.tableName = 'Users'; // specify the table for your seeds
return queryInterface.bulkInsert(options, [
{
email: 'demo@user.io',
username: 'Demo-lition',
hashedPassword: bcrypt.hashSync('password')
},
// ...
], {});
},
async down (queryInterface, Sequelize) {
options.tableName = 'Users';
const Op = Sequelize.Op;
return queryInterface.bulkDelete(options, {
username: { [Op.in]: ['Demo-lition'] }
}, {});
}
};
Again, we pass options (with schema and tableName)
as the first argument to bulkInsert or bulkDelete.
Model.bulkCreate
If you’re using Model.bulkCreate to seed data, you do not need
to specify the schema in your seed file. That’s because the Model in Sequelize
already knows which schema it belongs to (based on your production configuration).
An example:
async up (queryInterface, Sequelize) {
await User.bulkCreate([
{
email: 'demo@user.io',
username: 'Demo-lition',
hashedPassword: bcrypt.hashSync('password')
},
], { validate: true });
}
Since the User model is configured to use the right schema in production,
you don’t need to pass any special options about the schema.
With these changes, each of your Express projects deployed to Render can share one big database instance but still keep their data separate via schemas. This approach is crucial on Render’s free tier, which doesn’t give you multiple databases.
By applying the options object properly,
you ensure your tables and data land in the correct “sub-room” of that single database,
so they don’t conflict with other apps.
options object:
Put options.schema if NODE_ENV === 'production' at the top of every migration/seeder.
createTable calls:
Pass options as the third argument.
queryInterface methods (addColumn, removeColumn, etc.):
Add tableName to options, and pass it as the first argument.
bulkInsert or bulkDelete,
pass options as first arg. If using Model.bulkCreate,
you rely on the model config instead.
With these best practices, your migrations and seeders will run smoothly in production, letting each project maintain its own data within a single Render database. That means fewer conflicts, easy maintenance, and a more seamless overall experience.