@plantagoai/db
Schema-first Firestore database tools: Zod-based schema definitions, 3-phase validation, auto-fix, JSON export, batch cleanup, and security rules generation.
Consumers: Foundation (extracted), all projects
Installation
"@plantagoai/db": "file:../../shared/packages/db"
Peer Dependencies
| Dependency | Required For |
|---|---|
firebase-admin |
All database operations |
zod |
Schema definitions |
Schema Definition
Define your Firestore collections once — get validation, fixes, security rules, and export for free.
import { defineCollection } from "@plantagoai/db";
import { z } from "zod";
const proposals = defineCollection({
name: "proposals",
tenantScoped: true,
schema: z.object({
title: z.string().min(1),
status: z.enum(["round0", "active", "passed", "rejected"]),
scope: z.enum(["city", "regional", "national", "global"]).optional(),
support_count: z.number().int().nonnegative(),
total_votes: z.number().int().nonnegative(),
options: z.array(z.object({
id: z.string(),
label: z.string(),
votes: z.number().int().nonnegative(),
})),
created_at: z.string(), // ISO date
}),
refs: {
"votes.proposal_id": "proposals", // votes reference proposals
},
aggregates: {
"total_votes": { count: "votes", where: { proposal_id: "$id" } },
"options.$.votes": { count: "votes", where: { proposal_id: "$id", option_id: "$.id" } },
"support_count": { count: "supporter_signatures", where: { proposal_id: "$id" } },
},
});
const voters = defineCollection({
name: "voters",
tenantScoped: true,
schema: z.object({
wallet_address: z.string().min(1),
status: z.enum(["active", "suspended", "pending"]),
is_verified: z.boolean(),
is_biometric_verified: z.boolean(),
age: z.number().int().nonnegative().optional(),
registered_at: z.string(),
}),
});
CollectionConfig
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Firestore collection name |
tenantScoped |
boolean |
Yes | Whether docs have a tenant_id field |
schema |
ZodSchema |
Yes | Zod schema for document validation |
refs |
Record<string, string> |
No | Foreign-key references ("field": "targetCollection") |
aggregates |
AggregateDef[] |
No | Computed count fields to verify |
Database Validation
3-phase validation matching Foundation's existing Admin Tools validator:
import { validateDb } from "@plantagoai/db";
const report = await validateDb([proposals, voters, votes, funds], {
phases: [1, 2, 3], // which phases to run (default: all)
limit: 1000, // max docs per collection (default: unlimited)
onProgress: (msg) => console.log(msg),
db: adminDb, // optional Firestore override
});
console.log(`Errors: ${report.totalErrors}, Warnings: ${report.totalWarnings}`);
for (const col of report.collections) {
console.log(`${col.name}: ${col.docCount} docs, ${col.issues.length} issues`);
}
Phase 1 — Schema Validation
Validates every document against its Zod schema:
- Required fields present
- Field types correct
- Enum values valid
- Numeric constraints (non-negative, integer)
- String constraints (min length)
Phase 2 — Referential Integrity
Checks that foreign-key references point to existing documents:
- Votes → proposals (valid proposal_id and option_id)
- Supporter signatures → proposals
- Distributions → funds
- Identity proofs → voters
Phase 3 — Aggregate Consistency
Cross-validates computed fields against actual counts:
proposal.total_votesmatches vote doc countproposal.options[x].votesmatches per-option vote countproposal.support_countmatches supporter signature count- Detects duplicate votes/supports (same anonymous_hash)
ValidationReport
interface ValidationReport {
totalErrors: number;
totalWarnings: number;
collections: CollectionReport[];
}
interface CollectionReport {
name: string;
docCount: number;
issues: ValidationIssue[];
}
interface ValidationIssue {
collection: string;
docId: string;
phase: 1 | 2 | 3;
severity: "error" | "warning";
message: string;
field?: string;
}
Auto-Fix
Automatically repair common data issues:
import { fixDb } from "@plantagoai/db";
// Dry run first
const actions = await fixDb([proposals, voters], {
dryRun: true,
fixes: ["missing-defaults", "enum-normalize", "orphan-cleanup", "aggregate-recount"],
});
for (const action of actions) {
console.log(`${action.kind} on ${action.collection}/${action.docId}: ${action.field}`);
console.log(` Before: ${action.before} → After: ${action.after}`);
console.log(` Applied: ${action.applied}`); // false in dry run
}
// Apply fixes
const applied = await fixDb([proposals, voters], { dryRun: false });
Fix Types
| Fix Kind | Description |
|---|---|
missing-defaults |
Add missing fields with schema defaults |
enum-normalize |
Fix enum values (case normalization, trim) |
orphan-cleanup |
Delete docs whose foreign-key target doesn't exist |
aggregate-recount |
Recompute aggregate count fields from actual data |
timestamp-repair |
Fix malformed timestamp/date fields |
Export
Export collections to JSON:
import { exportDb } from "@plantagoai/db";
const result = await exportDb([proposals, voters, funds], {
format: "json",
includeMetadata: true, // adds _exportedAt, _collectionName
tenantId: "org-1", // filter by tenant (optional)
limit: 500, // max docs per collection
});
console.log(result.filename); // "export-2026-04-16T10-00.json"
console.log(result.json); // JSON string
for (const col of result.collections) {
console.log(`${col.name}: ${col.count} docs`);
}
Batch Cleanup
Delete documents with progress tracking:
import { cleanDb } from "@plantagoai/db";
const result = await cleanDb([proposals, voters, votes], {
tag: "seed-v1", // only delete docs with this _seed_tag (optional)
batchSize: 200, // docs per batch (default: 500)
onProgress: (msg) => console.log(msg),
});
console.log(`Deleted ${result.totalDeleted} documents`);
Security Rules Generation
Generate Firestore security rules from schema definitions:
import { generateRules } from "@plantagoai/db";
const rules = generateRules([proposals, voters, votes], {
tenantField: "tenant_id",
ringField: "ring",
});
console.log(rules);
// Output: Firestore rules string with:
// - Type validation per field
// - Tenant isolation (read/write scoped to user's tenant)
// - Ring-based access control
// - Required field enforcement
Foundation's Firestore Collections
The following collections are defined and validated in Foundation:
| Collection | Key Fields | Refs | Aggregates |
|---|---|---|---|
proposals |
title, status, scope, options[], support_count, total_votes | — | total_votes, options.votes, support_count |
voters |
wallet_address, status, is_verified, is_biometric_verified, age | — | — |
votes |
proposal_id, option_id, anonymous_hash | → proposals | — |
supporter_signatures |
proposal_id, anonymous_hash | → proposals | — |
voting_rounds |
round_type, status | → proposals | — |
funds |
status, categories[], balance | — | — |
distributions |
fundId, status | → funds | — |
product_requests |
status, bids[] | → proposals (optional) | — |
savings_summary |
Singleton ("current"), numeric fields | — | — |
identity_proofs |
proofType, trustTier | → voters | — |