Spaces & Multi-tenancy

The isolation unit — everything belongs to a space. RLS enforcement, user context, tenant boundaries.

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) where isDefault = true.
  • Space-level settings — Agents, reports, and wiki generation are configured per space, not globally.
  • Two DB connectionsgetDb() connects as app_user (RLS enforced); getAdminDb() connects as superuser (bypasses RLS). Application code should always use getDb() via withUserContext().

Data Model

ColumnTypeNotes
iduuid (PK)Primary key
userIduuidOwner of the space
nametextDisplay name
icontextDefault "folder"
isDefaultbooleanOne default space per user
agentsEnabledbooleanWhether agents can run in this space (default false)
agentsGeneratedAttimestampLast time agents were auto-suggested
feedsAnalyzedAttimestampLast feed analysis pass
feedsReevaluatedAttimestampLast feed re-evaluation pass
pendingFeedHinttextHint for next feed suggestion run
reportCadencetextHow often reports are generated
reportConfigjsonbReport generation configuration
reportSchedulesjsonbScheduled report definitions
wikiGeneratedAttimestampLast wiki generation
wikiStatustextCurrent wiki generation status
createdAttimestamp
updatedAttimestamp

Indexes:

IndexColumnsCondition
Unique partial(userId, isDefault)WHERE isDefault = true

How It Works

  1. User signs up — A default space is created automatically with isDefault: true.
  2. Request arrives — The API extracts the authenticated user's ID from the session.
  3. RLS context setwithUserContext(userId, async (tx) => { ... }) wraps the database operation in a transaction that calls SET LOCAL app.current_user_id = userId.
  4. 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.
  5. 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 VarDescription
DATABASE_URLPostgres connection string for app_user role (RLS enforced)
DATABASE_ADMIN_URLPostgres connection string for superuser (bypasses RLS)

Code Reference

FileDescription
packages/db/src/schema/spaces.tsSpace table definition and indexes
packages/db/src/rls.tswithUserContext() — sets RLS context per transaction
packages/db/src/client.tsgetDb() and getAdminDb() connection pool factories
docker/postgres/init.sqlAuth schema stubs, app_user role, base grants

Relationships

Making the unknown, known.

© 2026 Condelo. All rights reserved.