"Who needs a proper database when you have Excel's cooler sibling?"
Transform Google Sheets into a surprisingly capable database with SheetsORM - the ORM that makes your spreadsheet dreams come true (and your DBA nightmares). Built with TypeScript and Bun for developers who think outside the SELECT * FROM box.
Because why use boring, traditional databases when you can have:
Tired of accidentally running DROP TABLE users; in production? With Google Sheets, your data lives in the cloud with automatic version history. Every change is tracked, every mistake is reversible. Your junior developer can't destroy the database anymore - they'll just create a very confusing spreadsheet entry.
Finally! A database your non-technical stakeholders can actually view and edit. Watch as your product manager directly updates user data while your application is running. Experience the thrill of real-time collaborative database editing where business users can fix typos, add notes, and "improve" your carefully normalized data structure.
Instantly turn your database into charts, pivot tables, and AppSheet applications without writing a single line of BI code. Your data automatically becomes available to Google's entire ecosystem - from Data Studio dashboards to AI-powered analysis. Who needs expensive business intelligence tools when you have Google Sheets?
For when reality hits and you actually need a real database, SheetsORM provides bidirectional sync with PostgreSQL. Keep the best of both worlds - spreadsheet accessibility for humans, SQL power for your applications.
- π Google Sheets as Primary Database: Because spreadsheets are databases, right?
- π Bidirectional PostgreSQL Sync: Have your cake and eat it too
- ποΈ Multi-Table Support: One sheet, multiple tables (Excel veterans rejoice)
- π SQL Emulation: Run SQL queries on your spreadsheet like it's 2024
- π οΈ Full ORM Features: Models, relationships, migrations, and validations
- π TypeScript + Zod: Type safety for your chaotic data
- β‘ Bun Powered: Fast enough to make you forget you're using a spreadsheet
- π Version History: Time travel for your data mistakes
bun add sheetsormBecause even spreadsheets need authentication these days:
- Create a Google Cloud Project - Visit Google Cloud Console
- Enable the Google Sheets API - Because you need permission to programmatically mess with spreadsheets
- Create a Service Account - Download the JSON credentials (guard them with your life)
- Share your Google Sheet - Give your service account email edit access to your sheet
import { SheetsORM, z } from 'sheetsorm';
import type { ModelBase, TableSchema } from 'sheetsorm';
// Define your model (just like a regular ORM, but more exciting)
interface User extends ModelBase {
id: string;
name: string;
email: string;
age: number;
active: boolean;
department: string;
}
// Define the schema (your spreadsheet's structure)
const userSchema: TableSchema = {
name: 'users',
sheetName: 'Users', // The actual sheet tab name
primaryKey: 'id',
columns: [
{ name: 'id', type: 'string', required: true },
{ name: 'name', type: 'string', required: true },
{
name: 'email',
type: 'string',
required: true,
unique: true,
validation: z.string().email() // Because even spreadsheets deserve validation
},
{
name: 'age',
type: 'number',
validation: z.number().min(0).max(150) // No 200-year-old users, please
},
{ name: 'active', type: 'boolean', defaultValue: true },
{ name: 'department', type: 'string' },
{ name: 'created_at', type: 'date', required: true },
{ name: 'updated_at', type: 'date', required: true }
],
indexes: ['email', 'department'] // For faster lookups (sort of)
};
// Initialize your spreadsheet database
const orm = new SheetsORM({
googleSheets: {
credentials: {
type: 'service_account',
project_id: 'your-project-id',
private_key_id: 'your-private-key-id',
private_key: 'your-private-key',
client_email: 'your-service-account@your-project.iam.gserviceaccount.com',
client_id: 'your-client-id',
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
token_uri: 'https://oauth2.googleapis.com/token'
},
spreadsheetId: 'your-google-sheet-id'
},
postgresql: {
connectionString: 'postgresql://user:password@localhost:5432/database',
syncEnabled: true
},
syncDirection: 'bidirectional' // Best of both worlds
});
async function demonstrateTheAwesome() {
await orm.connect();
// Define repository (your spreadsheet table handler)
const userRepo = orm.defineModel<User>(userSchema);
// Setup your spreadsheet schema
await orm.createSheetsHeaders(); // Creates the header row
await orm.createTablesInPostgreSQL(); // Also creates PostgreSQL tables
// CRUD Operations (Create, Read, Update, Delete)
// Create a user (adds a new row to your sheet)
const newUser = await userRepo.create({
name: 'Alice Johnson',
email: 'alice@company.com',
age: 28,
department: 'Engineering',
active: true
});
console.log('Created user:', newUser.id);
// Find users with conditions (smart spreadsheet filtering)
const engineers = await userRepo.find({
where: { department: 'Engineering', active: true },
orderBy: ['name'],
limit: 10
});
// Update a user (modifies the spreadsheet row)
await userRepo.update(newUser.id!, {
age: 29,
department: 'Senior Engineering'
});
// Advanced querying
const activeUsers = await userRepo.findBy({ active: true });
const userCount = await userRepo.count({ department: 'Engineering' });
// Sync with PostgreSQL (for when you need real database features)
await orm.syncAll('bidirectional');
await orm.disconnect();
}
demonstrateTheAwesome().catch(console.error);What just happened? You've successfully turned a Google Sheet into a database with proper CRUD operations, validation, and PostgreSQL sync. Your data now lives where your business team can see it, edit it, and collaborate on it in real-time.
Yes, you can run actual SQL queries on your Google Sheets. It's like having a database, but with more colors and conditional formatting.
import { SQLEmulator } from 'sheetsorm';
const sqlEmulator = new SQLEmulator(orm.getSheetsAdapter(), orm.getSchemas());
// Query your spreadsheet like it's PostgreSQL
const topPerformers = await sqlEmulator.executeSQL(`
SELECT name, email, department, age
FROM users
WHERE active = true AND department = 'Sales'
ORDER BY created_at DESC
LIMIT 10
`);
// Complex queries work too
const departmentStats = await sqlEmulator.executeSQL(`
SELECT department, COUNT(*) as total_employees
FROM users
WHERE active = true
GROUP BY department
ORDER BY total_employees DESC
`);
// INSERT, UPDATE, DELETE operations
await sqlEmulator.executeSQL(`
INSERT INTO users (name, email, department)
VALUES ('Bob Smith', 'bob@company.com', 'Marketing')
`);
await sqlEmulator.executeSQL(`
UPDATE users
SET department = 'Senior Engineering'
WHERE department = 'Engineering' AND age > 30
`);Supported SQL Operations:
SELECTwith WHERE, ORDER BY, LIMITINSERTwith VALUESUPDATEwith WHERE conditionsDELETEwith WHERE conditionsCOUNT(*)aggregations- Basic JOINs (coming soonβ’)
Manage your spreadsheet structure like a proper database with version-controlled migrations.
import { MigrationBuilder } from 'sheetsorm';
// Create structured migrations for your spreadsheet schema
const migration_001 = new MigrationBuilder()
.createTable('users', userSchema)
.createTable('companies', companySchema)
.build('001', 'create_initial_tables');
const migration_002 = new MigrationBuilder()
.addColumn('users', {
name: 'avatar_url',
type: 'string',
validation: z.string().url().optional()
})
.addColumn('users', {
name: 'salary',
type: 'number',
validation: z.number().min(0)
})
.build('002', 'add_user_profile_fields');
const migration_003 = new MigrationBuilder()
.alterColumn('users', 'department', {
name: 'department',
type: 'string',
validation: z.enum(['Engineering', 'Sales', 'Marketing', 'HR'])
})
.dropColumn('users', 'old_field')
.build('003', 'standardize_departments');
// Run migrations (creates tracking in a special _migrations sheet)
const migrationRunner = orm.getMigrationRunner();
await migrationRunner.runMigrations([
migration_001,
migration_002,
migration_003
]);
// Check migration status
const status = await migrationRunner.getMigrationStatus();
console.log('Applied migrations:', status);
// Rollback if needed (careful with this!)
await migrationRunner.rollbackMigrations(1);Migration Features:
- β Create/drop tables (sheets)
- β Add/remove columns
- β Alter column types and validations
- β Migration history tracking
- β Rollback support
- β Automatic schema sync with PostgreSQL
Because even spreadsheets deserve proper data modeling.
import { RelationshipManager } from 'sheetsorm';
// Define related models
interface Company extends ModelBase {
id: string;
name: string;
industry: string;
}
interface Post extends ModelBase {
id: string;
title: string;
content: string;
user_id: string;
published: boolean;
}
// Setup repositories
const companyRepo = orm.defineModel<Company>(companySchema);
const postRepo = orm.defineModel<Post>(postSchema);
// Define relationships between your spreadsheet tables
const userRelations = new RelationshipManager(userRepo);
// One-to-Many: User has many posts
const userPosts = userRelations.hasMany('posts', postRepo, {
foreignKey: 'user_id'
});
// Many-to-One: User belongs to a company
const userCompany = userRelations.belongsTo('company', companyRepo, {
foreignKey: 'company_id'
});
// One-to-One: User has one profile (if you had a profile sheet)
const userProfile = userRelations.hasOne('profile', profileRepo, {
foreignKey: 'user_id'
});
// Load relationships (with automatic data fetching)
const user = await userRepo.findOne('user-123');
const userWithRelations = await userRelations.loadWith(user, [
'posts',
'company'
]);
console.log(userWithRelations.company.name); // 'Acme Corp'
console.log(userWithRelations.posts.length); // 5
// Manage relationships
await userPosts.add(user, newPost); // Associate a post
await userCompany.associate(user, company); // Set user's company
await userPosts.remove(user, oldPost); // Remove associationORM Features:
- β Models & Repositories: Type-safe data access patterns
- β Validation: Zod-powered data validation
- β Relationships: HasOne, HasMany, BelongsTo, BelongsToMany
- β Query Builder: Fluent API for complex queries
- β Hooks: Lifecycle events for data changes
- β Type Safety: Full TypeScript support
- β Error Handling: Graceful failure modes
Keep your spreadsheet and PostgreSQL database in perfect harmony (most of the time).
// Full sync between Google Sheets and PostgreSQL
await orm.syncAll('bidirectional');
// Granular sync with conflict resolution
await userRepo.sync({
direction: 'sheets-to-pg', // or 'pg-to-sheets' or 'bidirectional'
conflictResolution: 'sheets-wins', // Who wins when data conflicts?
batchSize: 100 // Process in chunks for large datasets
});
// Handle sync conflicts like a pro
const syncResult = await userRepo.sync({
direction: 'bidirectional',
conflictResolution: 'merge' // Merge based on updated_at timestamps
});
console.log(`Synced: ${syncResult.synced} records`);
console.log(`Conflicts: ${syncResult.conflicts.length}`);
console.log(`Errors: ${syncResult.errors.length}`);
// Manual conflict resolution
for (const conflict of syncResult.conflicts) {
console.log(`Conflict for user ${conflict.id}:`);
console.log('Sheets data:', conflict.sheetsData);
console.log('PostgreSQL data:', conflict.pgData);
console.log('Conflicting fields:', conflict.conflictFields);
// You decide which version to keep
}Sync Features:
- β Real-time Sync: Changes propagate automatically
- β Conflict Resolution: Multiple strategies for data conflicts
- β Batch Processing: Handle large datasets efficiently
- β Error Recovery: Graceful handling of sync failures
- β Selective Sync: Sync specific tables or records
- β Audit Trail: Track all sync operations
SheetsORM leverages the full power of the Google Sheets API to provide database-like functionality on your spreadsheets.
// Direct Google Sheets API access when you need it
const sheetsAdapter = orm.getSheetsAdapter();
// Batch operations for performance
await sheetsAdapter.batchUpdate([
{ range: 'Users!A1:Z100', values: userData },
{ range: 'Companies!A1:Z50', values: companyData }
]);
// Advanced sheet manipulation
await sheetsAdapter.createConditionalFormatting({
range: 'Users!D:D', // Age column
condition: { type: 'NUMBER_GREATER', value: 65 },
format: { backgroundColor: { red: 1.0, green: 0.6, blue: 0.6 } }
});
// Use Google Sheets formulas in your data
const calculatedData = await sheetsAdapter.updateFormulas({
'E2': '=SUM(D2:D100)', // Total age
'F2': '=COUNTIF(C:C,"true")', // Count active users
'G2': '=AVERAGE(D:D)' // Average age
});Google Sheets API Features:
- β Real-time Collaboration: Multiple users editing simultaneously
- β Formula Support: Use spreadsheet formulas for calculations
- β Conditional Formatting: Visual data representation
- β Data Validation: Built-in spreadsheet validation rules
- β Revision History: Complete audit trail of all changes
- β Comments & Notes: Collaborative annotations on data
- β Charts & Pivot Tables: Instant data visualization
- β Import/Export: CSV, Excel, PDF format support
"We need a database, but we also need our business team to manage the data directly"
// Perfect for: User management, product catalogs, order tracking
const startupORM = new SheetsORM({
googleSheets: { credentials, spreadsheetId: 'startup-users-db' },
syncDirection: 'sheets-to-pg' // Start with sheets, migrate to PG later
});"Our analysts need to update customer data, but developers need API access"
// Perfect for: Customer data, sales tracking, inventory management
const enterpriseORM = new SheetsORM({
googleSheets: { credentials, spreadsheetId: 'customer-master-data' },
postgresql: { connectionString: pgUrl, syncEnabled: true },
syncDirection: 'bidirectional' // Everyone stays in sync
});"We need real-time campaign data that marketing can edit and developers can query"
// Perfect for: Campaign metrics, lead tracking, A/B test results
const marketingDB = new SheetsORM({
googleSheets: { credentials, spreadsheetId: 'campaign-data-2024' },
syncDirection: 'sheets-to-pg' // Marketing owns the data
});"We need feature flags and config that non-technical team can modify"
// Perfect for: Feature flags, app configuration, content management
const configORM = new SheetsORM({
googleSheets: { credentials, spreadsheetId: 'app-config' },
postgresql: { connectionString: pgUrl, syncEnabled: true },
syncDirection: 'bidirectional'
});const sheetsConfig = {
googleSheets: {
credentials: {
type: 'service_account',
project_id: 'your-project-id',
private_key_id: 'key-id',
private_key: '-----BEGIN PRIVATE KEY-----\n...',
client_email: 'service-account@project.iam.gserviceaccount.com',
client_id: 'client-id',
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
token_uri: 'https://oauth2.googleapis.com/token'
},
spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms' // Your sheet ID
}
};const postgresConfig = {
postgresql: {
connectionString: 'postgresql://username:password@localhost:5432/database',
syncEnabled: true,
ssl: { rejectUnauthorized: false }, // For cloud databases
pool: {
min: 2,
max: 10,
idleTimeoutMillis: 30000
}
}
};const syncConfig = {
syncDirection: 'bidirectional', // 'sheets-to-pg' | 'pg-to-sheets' | 'bidirectional'
conflictResolution: 'merge', // 'sheets-wins' | 'pg-wins' | 'merge' | 'error'
batchSize: 100, // Records per batch
syncInterval: 5000, // Auto-sync interval (ms)
retryAttempts: 3, // Retry failed operations
enableRealTimeSync: true // Real-time change detection
};Sync Strategies Explained:
- π
sheets-wins: "The customer is always right" - Sheets data overwrites PostgreSQL - ποΈ
pg-wins: "Trust the database" - PostgreSQL data overwrites Sheets - π€
merge: "Let's compromise" - Merge based onupdated_attimestamps - π¨
error: "Stop everything!" - Throw errors on conflicts for manual resolution
const comprehensiveSchema: TableSchema = {
name: 'example',
sheetName: 'Example',
primaryKey: 'id',
columns: [
// String types with validation
{
name: 'email',
type: 'string',
required: true,
unique: true,
validation: z.string().email()
},
{
name: 'status',
type: 'string',
validation: z.enum(['active', 'inactive', 'pending'])
},
// Number types
{
name: 'price',
type: 'number',
validation: z.number().positive().max(1000000)
},
{
name: 'rating',
type: 'number',
validation: z.number().min(1).max(5)
},
// Boolean types
{
name: 'is_premium',
type: 'boolean',
defaultValue: false
},
// Date types
{
name: 'birth_date',
type: 'date',
validation: z.date().max(new Date()) // No future birthdays
},
// JSON types (stored as strings in sheets)
{
name: 'metadata',
type: 'json',
validation: z.object({
tags: z.array(z.string()),
settings: z.record(z.unknown())
})
}
]
};// Custom validation functions
const userSchema: TableSchema = {
name: 'users',
sheetName: 'Users',
primaryKey: 'id',
columns: [
{
name: 'username',
type: 'string',
validation: z.string()
.min(3, "Username too short")
.max(20, "Username too long")
.regex(/^[a-zA-Z0-9_]+$/, "Only alphanumeric and underscore allowed")
},
{
name: 'phone',
type: 'string',
validation: z.string()
.regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format")
.optional()
},
{
name: 'salary',
type: 'number',
validation: z.number()
.min(20000, "Salary too low")
.max(1000000, "Salary suspiciously high")
.multipleOf(1000, "Salary should be rounded to thousands")
}
]
};Organize your data like a spreadsheet ninja:
// Time-based partitioning
const users2023 = orm.defineModel<User>({
...userSchema,
sheetName: 'Users_2023'
});
const users2024 = orm.defineModel<User>({
...userSchema,
sheetName: 'Users_2024'
});
// Region-based partitioning
const usersNA = orm.defineModel<User>({
...userSchema,
sheetName: 'Users_North_America'
});
const usersEU = orm.defineModel<User>({
...userSchema,
sheetName: 'Users_Europe'
});
// Environment-based separation
const prodUsers = orm.defineModel<User>({
...userSchema,
sheetName: 'Users_Production'
});
const stagingUsers = orm.defineModel<User>({
...userSchema,
sheetName: 'Users_Staging'
});
// Cross-sheet queries (when you need to query multiple sheets)
async function getAllUsers() {
const [users2023Data, users2024Data] = await Promise.all([
users2023.find(),
users2024.find()
]);
return [...users2023Data, ...users2024Data];
}See the examples/ directory for more comprehensive examples:
basic-usage.ts: Basic CRUD operationsadvanced-features.ts: Migrations, relationships, and SQL emulation
Main ORM class for managing connections and models.
connect(): Connect to Google Sheets and PostgreSQLdisconnect(): Close all connectionsdefineModel<T>(schema): Define a model repositorygetRepository<T>(name): Get an existing repositorysyncAll(direction?): Sync all modelscreateTablesInPostgreSQL(): Create PostgreSQL tablescreateSheetsHeaders(): Create Google Sheets headers
Repository class for CRUD operations on models.
find(options?): Find multiple recordsfindOne(id): Find a single record by IDfindBy(criteria): Find records by criteriacreate(data): Create a new recordupdate(id, data): Update an existing recorddelete(id): Delete a recordcount(criteria?): Count recordssync(options?): Sync this model
Turn your SheetsORM data into no-code applications instantly:
// Your data is automatically available in AppSheet
// Create mobile apps, workflows, and dashboards without coding
// SheetsORM keeps your app data in sync with your applicationCreate beautiful dashboards and reports:
- Connect Data Studio directly to your sheets
- Real-time analytics on your application data
- Share insights with stakeholders
- No ETL pipelines needed
Leverage Google's AI services on your data:
- Smart data insights with Google Analytics Intelligence
- Natural language queries with Dialogflow
- Machine learning predictions with AutoML
- Data validation with Google Cloud AI
- Read Operations: ~200ms average latency
- Write Operations: ~300ms average latency
- Batch Operations: ~50ms per record in batches of 100
- Sync Operations: Depends on data size and conflict resolution
- Google Sheets Limits: 10 million cells per spreadsheet
- API Rate Limits: 300 requests per minute per project
- Concurrent Users: 100 simultaneous editors per sheet
- File Size: Practical limit around 100MB per spreadsheet
- Use batch operations for bulk data changes
- Implement caching for frequently accessed data
- Consider pagination for large datasets
- Monitor API quotas in production
- Use PostgreSQL sync for performance-critical queries
"Insufficient Permission" Error:
# Make sure your service account has edit access to the sheet
# Share the sheet with your service account email"Rate Limit Exceeded":
// Implement exponential backoff
const config = {
retryAttempts: 3,
retryDelay: 1000,
batchSize: 50 // Reduce batch size
};"Sync Conflicts":
// Handle conflicts gracefully
const result = await userRepo.sync({
conflictResolution: 'merge',
onConflict: (conflict) => {
console.log('Manual resolution needed:', conflict);
}
});We welcome contributions from developers who believe spreadsheets can be databases too!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Add comprehensive tests
- Ensure TypeScript types are correct
- Update documentation
- Submit a pull request
git clone https://github.com/your-repo/sheetsorm
cd sheetsorm
bun install
bun test
bun run lintMIT License - Because good ideas should be free.
Disclaimer: SheetsORM is not responsible for the existential crisis you may experience when you realize spreadsheets make decent databases. Side effects may include: increased productivity, confused DBAs, and an irresistible urge to put everything in Google Sheets.
- Google Sheets API team for making this possible
- Bun for making JavaScript have Arch-like community
- Everyone who said "You can't use a spreadsheet as a database" (you motivated us)
Built with β€οΈ and a healthy dose of "why not?" using Bun v1.2.5 btw.