Overview
Spaces are the fundamental isolation unit in Condelo. Every entity in the platform — sources, feeds, agents, documents, threads — belongs to exactly one space. A space is owned by a single user, and Row-Level Security (RLS) ensures that users can only ever access data within their own spaces.
Each user gets one default space automatically. Additional spaces can be created to separate different projects, clients, or analytical contexts. Spaces carry their own configuration for agent scheduling, report generation, and wiki status.
Key Concepts
- Isolation boundary — All data belongs to a space. There is no cross-space data access.
- User ownership — Every space has a
userId. RLS policies filter all queries by this owner. - Default space — Each user has exactly one default space, enforced by a unique partial index on
(userId, isDefault)whereisDefault = true. - Space-level settings — Agents, reports, and wiki generation are configured per space, not globally.
- Two DB connections —
getDb()connects asapp_user(RLS enforced);getAdminDb()connects as superuser (bypasses RLS). Application code should always usegetDb()viawithUserContext().
Data Model
| Column | Type | Notes |
|---|---|---|
id | uuid (PK) | Primary key |
userId | uuid | Owner of the space |
name | text | Display name |
icon | text | Default "folder" |
isDefault | boolean | One default space per user |
agentsEnabled | boolean | Whether agents can run in this space (default false) |
agentsGeneratedAt | timestamp | Last time agents were auto-suggested |
feedsAnalyzedAt | timestamp | Last feed analysis pass |
feedsReevaluatedAt | timestamp | Last feed re-evaluation pass |
pendingFeedHint | text | Hint for next feed suggestion run |
reportCadence | text | How often reports are generated |
reportConfig | jsonb | Report generation configuration |
reportSchedules | jsonb | Scheduled report definitions |
wikiGeneratedAt | timestamp | Last wiki generation |
wikiStatus | text | Current wiki generation status |
createdAt | timestamp | |
updatedAt | timestamp |
Indexes:
| Index | Columns | Condition |
|---|---|---|
| Unique partial | (userId, isDefault) | WHERE isDefault = true |
How It Works
- User signs up — A default space is created automatically with
isDefault: true. - Request arrives — The API extracts the authenticated user's ID from the session.
- RLS context set —
withUserContext(userId, async (tx) => { ... })wraps the database operation in a transaction that callsSET LOCAL app.current_user_id = userId. - Query executes — RLS policies on every table filter rows by
app.current_user_id, ensuring the query only touches data in the user's spaces. - Response returned — The transaction commits (or rolls back on error), and the local setting is discarded.
Why It Works This Way
RLS Over Application-Level Filtering
Row-Level Security is enforced at the database level, not in application code. This means a bug in a service layer cannot accidentally leak data across users. Even if a query forgets a WHERE clause, Postgres itself filters the rows.
One Default Space Per User
The partial unique index guarantees exactly one default space per user at the database level. Application code does not need to check for duplicates — the constraint handles it.
Space-Scoped Configuration
Agent scheduling, report cadence, and wiki generation are all scoped to a space rather than to a user or globally. This allows users to run different analytical configurations for different projects without interference.
Configuration
| Env Var | Description |
|---|---|
DATABASE_URL | Postgres connection string for app_user role (RLS enforced) |
DATABASE_ADMIN_URL | Postgres connection string for superuser (bypasses RLS) |
Code Reference
| File | Description |
|---|---|
packages/db/src/schema/spaces.ts | Space table definition and indexes |
packages/db/src/rls.ts | withUserContext() — sets RLS context per transaction |
packages/db/src/client.ts | getDb() and getAdminDb() connection pool factories |
docker/postgres/init.sql | Auth schema stubs, app_user role, base grants |
Relationships
- Sources — Each source belongs to a space
- Feeds — Each feed belongs to a space
- Agents — Each agent belongs to a space and respects
agentsEnabled - Inferences & Signals — Scoped to a space
- Surfaces & Experiences — Scoped to a space