diff --git a/demo/mcp/db.ts b/demo/mcp/db.ts
index 8d5b1372f..8d776af49 100644
--- a/demo/mcp/db.ts
+++ b/demo/mcp/db.ts
@@ -22,6 +22,10 @@ sqlite.exec(`
dek TEXT NULL
);
+ -- Account lookup runs before every entity op; index its access pattern.
+ CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx
+ ON corsair_accounts (tenant_id, integration_id);
+
CREATE TABLE IF NOT EXISTS corsair_entities (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
diff --git a/demo/testing/drizzle/0001_entities_account_index.sql b/demo/testing/drizzle/0001_entities_account_index.sql
new file mode 100644
index 000000000..3922dff17
--- /dev/null
+++ b/demo/testing/drizzle/0001_entities_account_index.sql
@@ -0,0 +1,2 @@
+CREATE INDEX `corsair_accounts_tenant_integration_idx` ON `corsair_accounts` (`tenant_id`,`integration_id`);--> statement-breakpoint
+CREATE INDEX `corsair_entities_account_type_entity_idx` ON `corsair_entities` (`account_id`,`entity_type`,`entity_id`);
diff --git a/demo/testing/drizzle/meta/0001_snapshot.json b/demo/testing/drizzle/meta/0001_snapshot.json
new file mode 100644
index 000000000..fa71cd524
--- /dev/null
+++ b/demo/testing/drizzle/meta/0001_snapshot.json
@@ -0,0 +1,377 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "303100e3-b988-44e0-9bab-73bdaa722aaa",
+ "prevId": "a5e0ee72-cd9f-4f83-b44b-d0ebc1b17246",
+ "tables": {
+ "corsair_accounts": {
+ "name": "corsair_accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dek": {
+ "name": "dek",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "corsair_accounts_tenant_integration_idx": {
+ "name": "corsair_accounts_tenant_integration_idx",
+ "columns": ["tenant_id", "integration_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "corsair_accounts_integration_id_corsair_integrations_id_fk": {
+ "name": "corsair_accounts_integration_id_corsair_integrations_id_fk",
+ "tableFrom": "corsair_accounts",
+ "tableTo": "corsair_integrations",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "corsair_entities": {
+ "name": "corsair_entities",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "corsair_entities_account_type_entity_idx": {
+ "name": "corsair_entities_account_type_entity_idx",
+ "columns": ["account_id", "entity_type", "entity_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "corsair_events": {
+ "name": "corsair_events",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "corsair_integrations": {
+ "name": "corsair_integrations",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dek": {
+ "name": "dek",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "corsair_permissions": {
+ "name": "corsair_permissions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "plugin": {
+ "name": "plugin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "callback_url": {
+ "name": "callback_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "review_url": {
+ "name": "review_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/demo/testing/drizzle/meta/_journal.json b/demo/testing/drizzle/meta/_journal.json
index 2e34c00ea..33cd67ce6 100644
--- a/demo/testing/drizzle/meta/_journal.json
+++ b/demo/testing/drizzle/meta/_journal.json
@@ -8,6 +8,13 @@
"when": 1773529091251,
"tag": "0000_nasty_moondragon",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1787808505608,
+ "tag": "0001_entities_account_index",
+ "breakpoints": true
}
]
}
diff --git a/demo/testing/migration.sql b/demo/testing/migration.sql
index e4aebd574..1f0a5c168 100644
--- a/demo/testing/migration.sql
+++ b/demo/testing/migration.sql
@@ -41,6 +41,11 @@ CREATE TABLE IF NOT EXISTS corsair_events (
FOREIGN KEY (account_id) REFERENCES corsair_accounts(id)
);
+CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx
+ ON corsair_accounts (tenant_id, integration_id);
+CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx
+ ON corsair_entities (account_id, entity_type, entity_id);
+
CREATE TABLE IF NOT EXISTS corsair_permissions (
id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
diff --git a/demo/testing/src/db/schema.ts b/demo/testing/src/db/schema.ts
index 8cf1a6dd3..436dc44ed 100644
--- a/demo/testing/src/db/schema.ts
+++ b/demo/testing/src/db/schema.ts
@@ -1,4 +1,4 @@
-import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const corsair_integrations = sqliteTable('corsair_integrations', {
id: text('id')
@@ -15,40 +15,59 @@ export const corsair_integrations = sqliteTable('corsair_integrations', {
dek: text('dek'),
});
-export const corsair_accounts = sqliteTable('corsair_accounts', {
- id: text('id')
- .primaryKey()
- .$defaultFn(() => crypto.randomUUID()),
- created_at: integer('created_at', { mode: 'timestamp' })
- .notNull()
- .$defaultFn(() => new Date()),
- updated_at: integer('updated_at', { mode: 'timestamp' })
- .notNull()
- .$defaultFn(() => new Date()),
- tenant_id: text('tenant_id').notNull(),
- integration_id: text('integration_id')
- .notNull()
- .references(() => corsair_integrations.id),
- config: text('config', { mode: 'json' }),
- dek: text('dek'),
-});
+export const corsair_accounts = sqliteTable(
+ 'corsair_accounts',
+ {
+ id: text('id')
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ created_at: integer('created_at', { mode: 'timestamp' })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ updated_at: integer('updated_at', { mode: 'timestamp' })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ tenant_id: text('tenant_id').notNull(),
+ integration_id: text('integration_id')
+ .notNull()
+ .references(() => corsair_integrations.id),
+ config: text('config', { mode: 'json' }),
+ dek: text('dek'),
+ },
+ (table) => [
+ index('corsair_accounts_tenant_integration_idx').on(
+ table.tenant_id,
+ table.integration_id,
+ ),
+ ],
+);
-export const corsair_entities = sqliteTable('corsair_entities', {
- id: text('id')
- .primaryKey()
- .$defaultFn(() => crypto.randomUUID()),
- created_at: integer('created_at', { mode: 'timestamp' })
- .notNull()
- .$defaultFn(() => new Date()),
- updated_at: integer('updated_at', { mode: 'timestamp' })
- .notNull()
- .$defaultFn(() => new Date()),
- account_id: text('account_id').notNull(),
- entity_id: text('entity_id').notNull(),
- entity_type: text('entity_type').notNull(),
- version: text('version').notNull(),
- data: text('data', { mode: 'json' }).notNull(),
-});
+export const corsair_entities = sqliteTable(
+ 'corsair_entities',
+ {
+ id: text('id')
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ created_at: integer('created_at', { mode: 'timestamp' })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ updated_at: integer('updated_at', { mode: 'timestamp' })
+ .notNull()
+ .$defaultFn(() => new Date()),
+ account_id: text('account_id').notNull(),
+ entity_id: text('entity_id').notNull(),
+ entity_type: text('entity_type').notNull(),
+ version: text('version').notNull(),
+ data: text('data', { mode: 'json' }).notNull(),
+ },
+ (table) => [
+ index('corsair_entities_account_type_entity_idx').on(
+ table.account_id,
+ table.entity_type,
+ table.entity_id,
+ ),
+ ],
+);
export const corsair_events = sqliteTable('corsair_events', {
id: text('id')
diff --git a/docs/concepts/database.mdx b/docs/concepts/database.mdx
index d38970974..1267dc5b3 100644
--- a/docs/concepts/database.mdx
+++ b/docs/concepts/database.mdx
@@ -9,7 +9,7 @@ Every API call and webhook that flows through Corsair is stored in your database
## Get started
-Run the migration once, then pass your connection to `createCorsair({ database, ... })`. See [Quick Start](/quick-start) for a full setup example.
+Run the migration once, then pass your connection to `createCorsair({ database, ... })`. See [Quick Start](/quick-start) for a full setup example. If the tables already exist, run only the two `CREATE INDEX` statements.
@@ -145,6 +145,11 @@ CREATE TABLE IF NOT EXISTS corsair_permissions (
expires_at TEXT NOT NULL,
error TEXT
);
+
+CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx
+ ON corsair_accounts (tenant_id, integration_id);
+CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx
+ ON corsair_entities (account_id, entity_type, entity_id);
```
@@ -232,6 +237,11 @@ CREATE TABLE IF NOT EXISTS corsair_permissions (
expires_at TEXT NOT NULL,
error TEXT
);
+
+CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx
+ ON corsair_accounts (tenant_id, integration_id);
+CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx
+ ON corsair_entities (account_id, entity_type, entity_id);
```
@@ -263,7 +273,7 @@ psql $env:DATABASE_URL -f migration.sql
```ts src/server/db/schema.ts
-import { pgTable, text, jsonb, timestamp } from 'drizzle-orm/pg-core';
+import { pgTable, text, jsonb, timestamp, index } from 'drizzle-orm/pg-core';
export const corsairIntegrations = pgTable('corsair_integrations', {
id: text('id').primaryKey(),
@@ -282,7 +292,9 @@ export const corsairAccounts = pgTable('corsair_accounts', {
integrationId: text('integration_id').notNull().references(() => corsairIntegrations.id),
config: jsonb('config').notNull().default({}),
dek: text('dek'),
-});
+}, (table) => [
+ index('corsair_accounts_tenant_integration_idx').on(table.tenantId, table.integrationId),
+]);
export const corsairEntities = pgTable('corsair_entities', {
id: text('id').primaryKey(),
@@ -293,7 +305,9 @@ export const corsairEntities = pgTable('corsair_entities', {
entityType: text('entity_type').notNull(),
version: text('version').notNull(),
data: jsonb('data').notNull().default({}),
-});
+}, (table) => [
+ index('corsair_entities_account_type_entity_idx').on(table.accountId, table.entityType, table.entityId),
+]);
export const corsairEvents = pgTable('corsair_events', {
id: text('id').primaryKey(),
@@ -360,6 +374,7 @@ model CorsairAccount {
entities CorsairEntity[]
events CorsairEvent[]
+ @@index([tenantId, integrationId], map: "corsair_accounts_tenant_integration_idx")
@@map("corsair_accounts")
}
@@ -374,6 +389,7 @@ model CorsairEntity {
data Json @default("{}")
account CorsairAccount @relation(fields: [accountId], references: [id])
+ @@index([accountId, entityType, entityId], map: "corsair_entities_account_type_entity_idx")
@@map("corsair_entities")
}
diff --git a/docs/guides/dashboard.mdx b/docs/guides/dashboard.mdx
index 490e0d421..9af287a80 100644
--- a/docs/guides/dashboard.mdx
+++ b/docs/guides/dashboard.mdx
@@ -176,6 +176,11 @@ CREATE TABLE IF NOT EXISTS corsair_events (
status TEXT,
FOREIGN KEY (account_id) REFERENCES corsair_accounts(id)
);
+
+CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx
+ ON corsair_accounts (tenant_id, integration_id);
+CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx
+ ON corsair_entities (account_id, entity_type, entity_id);
```
@@ -200,6 +205,8 @@ Get-Content migration.sql | sqlite3 corsair.db
+If the tables already exist, run only the two `CREATE INDEX` statements.
+
diff --git a/docs/quick-start.mdx b/docs/quick-start.mdx
index 906997ed8..f09ef1186 100644
--- a/docs/quick-start.mdx
+++ b/docs/quick-start.mdx
@@ -136,6 +136,11 @@ CREATE TABLE IF NOT EXISTS corsair_permissions (
expires_at TEXT NOT NULL,
error TEXT
);
+
+CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx
+ ON corsair_accounts (tenant_id, integration_id);
+CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx
+ ON corsair_entities (account_id, entity_type, entity_id);
```
@@ -159,6 +164,8 @@ Get-Content migration.sql | sqlite3 corsair.db
+If the tables already exist, run only the two `CREATE INDEX` statements.
+
Using Postgres, Drizzle, or Prisma instead? See [Database](/concepts/database) for each option.