This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a Rust-based cloud storage microservices architecture built as a Cargo workspace with 80+ crates. The system
handles document storage, processing, search, communication, and email functionality.
When making changes, make sure to test the services individually before committing using cargo test -p {my_service}
from the repository root.
Core Storage Services:
document-storage-service: Main document storage APIdocument-cognition-service: Document analysis and processingsearch_service: Search functionality across documentsstatic_file_service: Static file serving
Processing Services:
convert_service: Document format conversiondocument-text-extractor: Text extraction from documentssearch_processing_service: Search indexing and processing
Communication Services:
email_service: Email processing and managementnotification_service: User notifications
Infrastructure Services:
authentication_service: User authenticationconnection_gateway: WebSocket gatewaycontacts_service: Contact management
The system uses multiple databases:
- MacroDB: Main PostgreSQL database for documents, users, projects, Communication data (messages, channels, participants), Email threads, messages, metadata, and notification preferences/history
- ContactsDB: User connections and contacts
External storage includes S3 for document files, Redis for caching, OpenSearch for search indexing, and DynamoDB for connection tracking.
DB migration files are located in crates/macro_db_client. Use the /dump-schema skill to dump the current Postgres schema for reference.
If you are still getting migration errors after running just setup_macrodb, you may need to run just force_drop_db
in crates/macro_db_client to drop the database and re-create it
with just setup_macrodb at the repository root. Remember that some database table and column names may be
camelCased rather than snake_cased (use /dump-schema or check the migration files for actual column names).
When a column is camelCased, you need to cast it as the snake_cased version when reading from the database. E.g.
SELECT "userId" as "user_id" FROM "UserInsights".
Any time you make changes to the SQL code in rust, you need to run just prepare_db to
update the .sqlx directory. Always run it inside nix develop and only from the
repository root (for example, nix develop --command just prepare_db) — do not run it from
individual crate directories anymore. The workspace-level recipe handles every crate that
has sqlx queries.
Use \cd instead of cd to navigate in the repository.
just build # Build all services
just build_lambdas # Build all Lambda functions
just check # Type check without buildingjust create_networks
just run_dbs -d
just setup_test_envs
just initialize_dbs
cargo test -p {crate}just test does not exist. Leave SQLX_OFFLINE unset when you run cargo test. Run just prepare_db only if you changed SQL queries.
Email rendering snapshots (Playwright HTML fixtures, not inbox e2e) live in apps/web/src/lib/core/email/tests. Run just test-email-rendering. Add a fixture under fixtures/ then just test-email-rendering-update.
cargo fmt # format
just clippy # extra lints / best practicesUse just setup_macrodb or just initialize_dbs to create and migrate MacroDB. Those recipes are the same.
Schemas live in crates/macro_db_client/migrations/.
To reset MacroDB, run just crates/macro_db_client/drop_db -y -f, then just setup_macrodb.
Individual lambda builds available for:
build_document_text_extractorbuild_docx_unzip_handlerbuild_delete_chat_handlerbuild_upload_extractor_lambda_handlerbuild_email_suppressionbuild_deleted_item_poller
Services communicate via:
- HTTP APIs (internal service clients)
- SQS queues for async processing
- Lambda triggers for event-driven processing
- Redis for caching and session management
- Each service has its own database client crate (e.g.,
macro_db_client,comms_db_client) - Uses SQLx for database interactions with offline query validation
- Migrations managed per service
Heavy use of AWS services:
- S3 for file storage
- Lambda for serverless processing
- SQS for message queuing
- DynamoDB for connection tracking
- OpenSearch for search capabilities
Environment variables are managed in doppler. New env vars should be added to doppler. All environment variables should always be loaded with the macros in the macro_env_var crate. They should never be loaded with std::env::var.
- Docker (for local databases)
sqlx-clifor database migrationsjustfor task running- Pulumi CLI for infrastructure
- AWS CLI for deployment
The project uses SQLX_OFFLINE=true for building without database connections. Database queries are pre-validated and
cached.
Documents go through: Upload → Text Extraction → Search Indexing → Storage → Retrieval
- DOCX files are unzipped via Lambda
- PDFs processed with pdfium library
- Text indexed in OpenSearch
- Metadata stored in PostgreSQL
This case study documents the process of extending message mentions to support generic entity mentions (e.g., documents mentioning other documents).
- Analyzed Requirements: Extended existing MessageMention functionality to support any entity mentioning any other entity
- Created Todo List: Used TodoWrite tool to track implementation steps
- Examined Existing Code: Reviewed current message_mentions table structure and usage
-
Data Model Changes
- Created
EntityMentionstruct with generic source/target fields - Maintained backward compatibility with existing mentions
- Created
-
Database Migration
- Renamed
message_mentions→entity_mentions - Added
source_entity_typeandsource_entity_idcolumns - Migrated existing data (messages) to new structure
- Updated all indexes for performance
- Renamed
-
Updated Database Client
- Created
entity_mentionsmodule with create/delete functions - Modified
get_attachment_referencesto query new table - Updated
create_message_mentionsto insert into new table - Fixed test fixtures to use new table structure
- Created
-
API Endpoints
- Created POST/DELETE
/entity-mentionsendpoints - Used proper Extension extractors for axum handlers
- Added OpenAPI documentation
- Created POST/DELETE
-
Compilation Issues
- Fixed import errors (wrong Context type, missing http import)
- Added Clone trait to structs used in tests
- Updated fixture references from
message_mentionstomentions
-
Test Failures
- Fixed
create_message_mentionstest by updating query logic - Query now returns all mentioned users, not just newly inserted ones
- Updated fixtures to include entity_mentions data
- Fixed
-
SQLX Offline Mode
- Encountered "no cached data" errors due to schema changes
- Required running migrations before
cargo sqlx prepare
- Run migrations:
just migrate_db - Update SQLX cache:
just prepare_db - Verify with tests:
cargo test
- Todo Management: Proactive use of TodoWrite helps track complex multi-step tasks
- Incremental Testing: Run tests frequently to catch issues early
- Fixture Management: Update test fixtures when changing table structures
- SQLX Workflow: Schema changes require migration → prepare → test cycle
- Axum Patterns: Handlers take shared services via
State, notExtension(see docs/STYLE_GUIDE.md CS-30; this case study predates that convention)
The migration included comprehensive indexes:
- Composite index on (entity_type, entity_id) for efficient lookups
- Index on source columns for reverse lookups
- Index on created_at for ordering
- Maintained existing performance optimizations
- Prefer SQLx compile-time checked macros (
query!,query_as!,query_scalar!) for database queries whenever possible instead of dynamicsqlx::querycalls. - Never manually create or edit
.sqlx/query-*.jsonfiles. To update SQLx query metadata, runjust prepare_dbfrom the repository root. - When creating a new SQLx migration file, run
sqlx migrate add <descriptive_name>from the relevant database crate (or use SQLx's--sourceoption) and then edit the generated file. Never manually create migration files, and never invent, copy, or guess timestamp prefixes to fake a migration filename. - Always run tests between changes that involve changes to db queries
- Never run
cargo testwithSQLX_OFFLINE=true. Tests are designed to validate against the live local Postgres; offline mode forces sqlx macros to consult the cached.sqlxdata and can either surface confusing "type annotations needed" errors when a query was not in the cache or hide regressions where a query no longer matches the schema. If tests fail with sqlx "no cached data" errors, runjust prepare_db(with--testswhen the failure is in test code) — do not flip offline mode on.SQLX_OFFLINE=trueis fine forcargo check/cargo build/cargo clippyonly.
- New code uses
rootcausefor error handling — it's preferred overanyhow(see docs/STYLE_GUIDE.md CS-46) - In code still on anyhow: prefer
anyhow::bail!("error message")overErr(anyhow::anyhow!("error message"))for early returns - it's more concise and idiomatic
docs/AGENT_GUIDE/ documents how agents drive the web app through a browser (routes, UI affordances, interaction patterns, completion signals). When you change how a part of the app works or how users/agents interact with it — routes, creation flows, editor behavior, AI surfaces, composer semantics — update the corresponding guide file in the same change.
- Add
#![deny(missing_docs)]tolib.rsin new crates to enforce documentation on all public items - This ensures all public functions, structs, enums, and modules have documentation comments
- Do not use
ignoreto except code blocks from doc tests unless explicitely directed
Place tests in a separate test.rs file within the same module directory, rather than inline with #[cfg(test)] blocks in the implementation file.
Pattern:
- Implementation:
foo/mod.rsorfoo.rs - Tests:
foo/test.rs
Note: You do NOT need to convert a file module (foo.rs) into a directory module (foo/mod.rs) to add tests.
Rust supports foo.rs alongside a foo/ directory — just create foo/test.rs and it works as a submodule of foo.rs.
Example structure:
src/
user.rs # Contains: mod test; (with #[cfg(test)]) + implementation
user/
test.rs # Contains: use super::*; and test functions
In user.rs:
#[cfg(test)]
mod test;
// implementation code...In test.rs:
use super::*;
#[tokio::test]
async fn test_something() {
// test code
}This keeps implementation files focused and makes tests easier to locate and maintain.
- Include
errwhen adding thetracing::instrumentattribute to functions that returnResult. Do not includeerron functions that returnOption,(), or other non-Resulttypes. Never includelevel = "info". - When including an error with a log, include it like so:
tracing::error!(error=?e, "error msg");don't inject it directly into the error message. - Prefer using
.inspect_errinstead ofif let Err(e)in order to do logging.
- When making changes to a db crate you should always update tests, and run prepare
These apply to Cursor Cloud only. On a local dev machine the .cursor/*.sh scripts prompt for sudo and are the wrong entry point: to verify a frontend change, run PORT=<free port> bun run dev from apps/web against the dev backend (see apps/web/AGENTS.md), and only reach for the local stack for backend work.
.cursor/install.sh prepares the durable caches, databases, test dependencies, frontend dependencies, service binaries, and stack init snapshot, then stops dockerd and nix-daemon so the Cloud bake can exit. .cursor/start.sh runs at boot and starts nothing beyond the nix daemon, so sessions and subagents are usable immediately. .cursor/infra.sh starts Docker, Postgres, and Redis on demand. .cursor/stack.sh starts the on-demand product: backend containers behind the proxy (8090) plus the hot-reloading frontend dev server — the app is at http://localhost:3000/app and frontend edits apply on save (idempotent; a healthy stack is left alone). .cursor/rebuild.sh remounts new backend binaries after Rust edits. These scripts are the only supported entry points; each re-enters the pinned nix shell itself, so run them with plain bash from any environment. To run the app and see your edits, follow the run-app skill (.claude/skills/run-app/SKILL.md).
Nix is the only host dependency. The pinned dev shell supplies the Docker CLI and daemon, Compose, fuse-overlayfs, and OpenSSH. ssh-keygen must come from this shell.
Service binaries come from the private S3 Nix cache. A sibling workflow on push to main (push_local_stack_binaries.yml) builds and pushes .#local-stack-binaries. It is not part of the deploy pipeline. Cursor Cloud realizes the aggregate with nix build .#local-stack-binaries.
Set NIX_CACHE_AWS_ACCESS_KEY_ID and NIX_CACHE_AWS_SECRET_ACCESS_KEY as Cursor environment secrets. The IAM credentials need read-only access to the cache bucket.
Set DOPPLER_TOKEN as a Cursor environment runtime secret: a Doppler service token scoped to the local project's lcl_preview config (the same token CI stores as DOPPLER_PREVIEW_TOKEN also works). Do not paste the token into chat. When the token is present, bash .cursor/stack.sh pulls those secrets instead of passing --no-doppler. Install/bake stays on stubs so the snapshot does not embed secrets. Existing running agents do not pick up newly added secrets — start a new agent after adding the token.
Nothing runs after boot. Before DB-backed cargo test -p <crate>, run bash .cursor/infra.sh once — it brings up Docker, Postgres, and Redis in seconds because install baked the images and volumes. Pure-logic crate tests need nothing. Run bash .cursor/stack.sh for a product-ready environment.
After backend edits, run bash .cursor/rebuild.sh. That nix-builds the stack binaries and runs just stack update --binaries-dir, which remounts them without wiping volumes.
No seeding or OTP is needed to log in: passwordless login auto-creates a user for any email, and the stack's auth service is built with return_passwordless_code, so the login API returns the code in its response (codes are also visible in Mailpit at http://localhost:8025). just seed-scenario apply --file seed/scenarios/team-perms.json is optional, for multi-user team/permission fixtures. The agent_harness_service restart loop is expected when AI provider keys are missing; with DOPPLER_TOKEN those keys come from local/lcl_preview.
Leave SQLX_OFFLINE unset for cargo test. If SQLx reports missing cached query data, run just prepare_db instead of enabling offline mode. Run crate tests from the repository root with cargo test -p <crate>.