M1, M2 and M3 complete * M1, M2 and M3 complete * review changes * docs * docs
15 KiB
Codex Plan vs Claude Plan - Comparison Analysis
Date: 2026-03-23 Purpose: Identify issues or strengths in the Codex plan that our Claude plan may have missed or should consider adopting.
1. Agreement / Shared Ground
Both plans converge on the same high-level design, which is reassuring:
- Additive hybrid approach: keep legacy fixed-column field extractions, add new JSONB-backed custom metadata
- Schema-as-data stored as JSON Schema documents in JSONB
- Immutable schema versions (new row per revision, new ID)
- Nullable schema reference on the
documentstable - Schema binding locks once metadata is written
- Application-layer JSON Schema validation (not DB-side
json_matches_schema()) DISTINCT ONview for current metadata version- No GIN index initially
- JSON Schema draft 2020-12
- Client-scoped schemas with cross-client isolation
- Version history for metadata records
These are the foundational decisions and both plans agree on all of them. No action needed.
2. Issues Codex Raises That We Should Evaluate
2.1 DB Triggers for Critical Invariants (STRONG - Consider Adopting)
What Codex says: Two database triggers should enforce invariants:
- Prevent
documents.metadataSchemaIdchanges after any extraction exists (legacy or custom) - Validate
documents.clientId = documentMetadataSchemas.clientIdon insert/update
Our plan: Relies entirely on application-layer enforcement.
Assessment: This is the strongest point in the Codex plan that we missed. Application-layer enforcement is correct but fragile -- a future developer could bypass the service layer (e.g., direct SQLC query, migration script, admin fix-up query) and silently violate the invariant. DB triggers make the invariant durable regardless of code path.
Risk of NOT adopting: A data integrity bug that is invisible until documents have mismatched schemas. Hard to detect, hard to fix retroactively.
Recommendation: Adopt. Add both triggers. The cost is two small trigger functions in the migration. The benefit is defense-in-depth for the two most critical invariants (schema lock and client isolation). This aligns with our existing pattern of using DB constraints as safety nets alongside app-layer checks.
2.2 Three-State Schema Status Enum (MODERATE - Consider Adopting)
What Codex says: Uses a metadataSchemaStatus enum with three values: active, superseded, retired.
Our plan: Uses a boolean is_active (true/false).
Assessment: The three-state model captures a meaningful distinction:
superseded: automatically set when a newer version is created -- the schema is "replaced" but not manually disabledretired: explicitly disabled by an admin -- different intent, possibly different business rules (e.g., retired schemas might warrant a warning in the UI)
With a boolean, both cases collapse to is_active = false and the system cannot distinguish "replaced by v2" from "admin decided this schema is bad."
Risk of NOT adopting: Minor. We can always add the distinction later. But it's cleaner to model it upfront since it costs nothing extra at the DB level and avoids a later migration.
Recommendation: Consider adopting. A three-state enum is a small change that gives us more precise semantics. If we keep the boolean, we should at minimum document that is_active = false covers both cases and note it as a known simplification.
2.3 schemaHash for Deduplication Detection (WEAK - Note But Skip)
What Codex says: Includes a schemaHash text NOT NULL column on the schema table.
Our plan: No hash column.
Assessment: Useful for detecting accidental duplicate submissions (same schema content submitted as a "new version"). Could also speed up equality checks. However, it adds complexity (what hash algorithm? is it normalized JSON before hashing?) and the use case is marginal for v1. An admin accidentally creating a duplicate version is a minor annoyance, not a data integrity issue.
Recommendation: Skip for now. Note it as a potential future addition. If duplicate detection becomes a real problem, we can add it without schema changes (just a new column + backfill).
2.4 supersedesSchemaId / Explicit Version Chain (WEAK - Skip)
What Codex says: Each schema row has a supersedesSchemaId uuid REFERENCES documentMetadataSchemas(id) creating an explicit linked-list chain of versions.
Our plan: Relies on (client_id, name, version) ordering to determine lineage. The PUT response includes previousVersionId but it's computed, not stored.
Assessment: Both approaches work. The explicit FK chain makes traversal trivial (SELECT * FROM ... WHERE supersedesSchemaId = ?) but (name, version) ordering is equally functional and simpler. An explicit chain also creates complexity around edge cases: what if a superseded schema is itself deleted? What if someone creates a revision of a superseded (not latest) version?
Recommendation: Skip. Our (name, version) ordering is simpler and sufficient. The lineage is already navigable via ORDER BY version.
2.5 documentClass Column (WEAK - Skip)
What Codex says: Includes a documentClass text column on the schema table for categorization.
Our plan: No document class column. The schema name and description serve the categorization role.
Assessment: This is a thin metadata field. It could be useful for filtering schemas by document type (e.g., "show me all schemas for aircraft engineering documents"). But name already serves this purpose, and adding a separate classification dimension without clear UI or API requirements is speculative.
Recommendation: Skip. Can be added later if a classification use case emerges. The schema name and description fields handle this for now.
2.6 Metadata Table Design: Two Tables vs One (MODERATE - Evaluate)
What Codex says: Uses two tables mirroring the existing legacy pattern:
documentMetadataExtractions(the actual JSONB data)documentMetadataExtractionVersions(version tracking)
Our plan: Uses a single document_custom_metadata table with a version column combining both concerns.
Assessment: This is a tradeoff:
| Aspect | Two-table (Codex) | One-table (Ours) |
|---|---|---|
| Consistency with existing code | Matches legacy documentFieldExtractions + documentFieldExtractionVersions pattern |
New pattern, different from legacy |
| Simplicity | More tables, more joins | Fewer tables, simpler queries |
| Row-level locking for version increment | Version rows can be locked independently of data rows | Must lock the last data row for version increment |
| Query complexity | Need JOIN for full data | Direct SELECT |
The existing codebase uses the two-table pattern for field extractions. Matching it means developers familiar with the legacy code can reason about the new code by analogy. Breaking from it means two different versioning patterns in the same codebase.
Risk of NOT adopting: Cognitive overhead for developers who expect the two-table pattern. Potential row-locking issues if we need to increment versions under concurrency (though our traffic patterns likely don't warrant this concern).
Recommendation: Evaluate. The consistency argument is real. However, our single-table approach is simpler and YAGNI applies -- we don't need the version table separation unless we have a concrete reason. If Q prefers consistency with the legacy pattern, we should switch. Otherwise, keep our simpler design and document why we deviated.
2.7 Mutual Exclusivity: Legacy OR Custom (MODERATE - Discuss with Q)
What Codex says: Strict mutual exclusivity:
- If
metadataSchemaId IS NULL, only legacy/field-extractionsendpoints work - If
metadataSchemaId IS NOT NULL, only custom/field-extractions/metadataendpoints work - Assigning a schema to a document with legacy extractions returns
409 Conflict
Our plan: A document can have BOTH static field extractions AND custom metadata. They are "independent and complementary."
Assessment: This is a fundamental design philosophy difference:
-
Codex (exclusive): Cleaner model, prevents confusion about which system is "authoritative" for a document. Simpler to reason about. But constrains flexibility -- what if a document needs both legacy fields AND custom fields during a transition period?
-
Ours (both allowed): More flexible. Allows gradual migration. But introduces ambiguity: if a document has both, which is the "real" metadata? Could lead to inconsistencies if different systems write to different stores for the same document.
Risk of our approach: A document could end up with conflicting data in legacy fields and custom metadata, with no clear source of truth. This is a data governance concern, not a technical one.
Recommendation: Discuss with Q. This is a business decision. If the expectation is that documents will gradually migrate from legacy to custom, allowing both may be needed during transition. If the expectation is a clean cut-over, mutual exclusivity is cleaner. Our plan should explicitly document whichever choice is made and why.
2.8 Route Structure: /field-extractions/... vs Split (MINOR)
What Codex says: All new endpoints under /field-extractions/... to stay aligned with existing Permit resource naming.
Our plan: Admin schemas under /admin/custom-schemas, metadata under /custom-metadata.
Assessment: Our split is intentional and defensible -- it separates admin-scoped operations from user-scoped operations, which maps cleanly to role-based access. The Codex approach keeps everything grouped under one resource family, which simplifies Permit configuration but mixes admin and user operations under the same prefix.
Recommendation: Keep our approach. The admin/user split is cleaner for RBAC. We already account for the Permit.io changes needed.
2.9 Schema Size Limit: 256KB vs 64KB (MINOR)
What Codex says: 256KB limit.
Our plan: 64KB limit with a tunable constant.
Assessment: Both are reasonable. 64KB is generous for a JSON Schema document (most schemas will be 1-5KB). 256KB is very generous. Since we made it a tunable constant (MaxSchemaDefinitionBytes), the exact value is easy to change.
Recommendation: Keep 64KB. It's more than enough, and a lower limit is a better default. If a client hits it, we bump the constant.
2.10 Schema Validation: additionalProperties Required (MINOR - Consider)
What Codex says: The server should reject schema creation unless additionalProperties is explicitly present at root level.
Our plan: additionalProperties: false is "recommended but not required."
Assessment: Requiring it prevents a common mistake -- submitting a schema without additionalProperties, which defaults to true in JSON Schema, meaning the schema accepts any extra fields. This is almost never what the client admin intends. Requiring explicit declaration forces a conscious choice.
Recommendation: Consider adopting. Requiring additionalProperties to be explicitly set (not necessarily false, just present) is a small validation rule that prevents a common gotcha. Low cost, moderate value.
2.11 Request Body Should Not Carry schemaId (ALREADY HANDLED)
What Codex says: The metadata write request body should not include schemaId -- the server derives it from the document's assigned schema.
Our plan: Same approach. The POST /custom-metadata request contains documentId and metadata but not schemaId. The response includes schemaId (server-derived).
Assessment: Both plans agree. No action needed.
2.12 Usage Stats in Schema Responses (MINOR - Already Handled Differently)
What Codex says: Schema detail response includes a usage block: assignedDocumentCount, metadataRecordCount, canDelete.
Our plan: Schema list/detail responses include documentCount.
Assessment: The Codex response is slightly richer (canDelete is convenient for UIs, metadataRecordCount distinguishes "assigned but no data yet" from "actively in use"). Our documentCount covers the main use case.
Recommendation: Consider adding canDelete. It's a simple boolean derived from the count and saves the client a mental step. metadataRecordCount is nice-to-have but not essential for v1.
3. Things Our Plan Has That Codex Missed
3.1 Bulk Folder Assignment
Our plan includes POST /admin/folders/{folderId}/assign-schema with recursive subfolder traversal, skip reporting, and transactional safety. Codex has no bulk operations at all. This is a significant operational feature for large clients.
3.2 client_admin Role
Our plan defines a new client_admin role with clear permission boundaries. Codex mentions the need for Permit.io updates but doesn't define a new role or permission model.
3.3 Detailed Service Layer Design
Our plan provides Go code outlines for the service, validator, and controller layers. Codex stays at the SQL/API level and doesn't describe the Go implementation structure.
3.4 Sample Schemas / Documentation
Our plan includes comprehensive sample schemas covering all supported JSON Schema types. Codex doesn't address documentation.
3.5 More Granular Implementation Phases
Our plan has 11 phases with explicit dependency tracking. Codex has 4 broad phases.
4. Summary: Recommended Actions
| # | Codex Issue | Strength | Action |
|---|---|---|---|
| 2.1 | DB triggers for invariants | STRONG | Adopt -- add triggers for schema lock + client match |
| 2.2 | Three-state schema status | MODERATE | Consider -- enum is cleaner than boolean |
| 2.3 | schemaHash |
Weak | Skip |
| 2.4 | supersedesSchemaId chain |
Weak | Skip |
| 2.5 | documentClass column |
Weak | Skip |
| 2.6 | Two-table metadata pattern | MODERATE | Discuss with Q -- consistency vs simplicity |
| 2.7 | Mutual exclusivity (legacy OR custom) | MODERATE | Discuss with Q -- business decision |
| 2.10 | Require additionalProperties present |
Minor | Consider -- prevents common mistake |
| 2.12 | canDelete in responses |
Minor | Consider -- small UX improvement |
Bottom line: The Codex plan is a competent alternative design that arrives at the same destination via slightly different choices. The one genuinely strong point we should adopt is DB triggers for critical invariants (2.1). The three moderate items (2.2, 2.6, 2.7) deserve a decision from Q. Everything else is either already covered in our plan or is a minor preference difference that doesn't materially affect the design.