Implementation Steps
1. Define the Schema
We use zod for runtime validation of the matrix configuration. This ensures data integrity before any graph operations.
import { z } from 'zod';
export const AssetSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
type: z.enum(['code', 'config', 'design', 'content']),
path: z.string(),
hash: z.string().optional(), // Computed hash for deduplication
license: z.string().optional(),
});
export const FeatureSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
status: z.enum(['idea', 'wip', 'shipped', 'deprecated']),
dependencies: z.array(z.string()).optional(), // References asset IDs
monetization: z.enum(['free', 'freemium', 'paid', 'oss']).optional(),
});
export const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
description: z.string().optional(),
features: z.array(z.string()).optional(), // References feature IDs
sharedAssets: z.array(z.string()).optional(), // Global assets
techStack: z.array(z.string()).optional(),
});
export const ProductMatrixSchema = z.object({
version: z.literal('1.0'),
products: z.array(ProductSchema),
features: z.array(FeatureSchema),
assets: z.array(AssetSchema),
metadata: z.object({
lastAudit: z.string().datetime().optional(),
totalLinesOfCode: z.number().optional(),
}).optional(),
});
export type ProductMatrix = z.infer<typeof ProductMatrixSchema>;
2. Build the Matrix Engine
The engine constructs the graph and provides analytical methods.
import { createHash } from 'crypto';
import { ProductMatrix, ProductMatrixSchema } from './schema';
import * as fs from 'fs/promises';
export class MatrixEngine {
private matrix: ProductMatrix;
private graph: Map<string, Set<string>> = new Map();
constructor(configPath: string) {
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
this.matrix = ProductMatrixSchema.parse(raw);
this.buildGraph();
}
private buildGraph(): void {
// Initialize nodes
for (const asset of this.matrix.assets) {
this.graph.set(asset.id, new Set());
}
for (const feature of this.matrix.features) {
this.graph.set(feature.id, new Set());
}
for (const product of this.matrix.products) {
this.graph.set(product.id, new Set());
}
// Build edges: Product -> Feature -> Asset
for (const product of this.matrix.products) {
product.features?.forEach(fid => {
this.addEdge(product.id, fid);
});
}
for (const feature of this.matrix.features) {
feature.dependencies?.forEach(aid => {
this.addEdge(feature.id, aid);
});
}
}
private addEdge(from: string, to: string): void {
if (!this.graph.has(from)) this.graph.set(from, new Set());
this.graph.get(from)!.add(to);
}
// Detect assets used by multiple products
getSharedAssets(): Array<{ assetId: string; productCount: number }> {
const usageMap = new Map<string, Set<string>>();
for (const product of this.matrix.products) {
const usedAssets = new Set<string>();
// Traverse graph to find assets for this product
this.traverse(product.id, (nodeId) => {
if (this.matrix.assets.some(a => a.id === nodeId)) {
usedAssets.add(nodeId);
}
});
usedAssets.forEach(aid => {
if (!usageMap.has(aid)) usageMap.set(aid, new Set());
usageMap.get(aid)!.add(product.id);
});
}
return Array.from(usageMap.entries())
.filter(([, products]) => products.size > 1)
.map(([assetId, products]) => ({
assetId,
productCount: products.size,
}))
.sort((a, b) => b.productCount - a.productCount);
}
private traverse(nodeId: string, callback: (id: string) => void, visited = new Set<string>()): void {
if (visited.has(nodeId)) return;
visited.add(nodeId);
callback(nodeId);
const neighbors = this.graph.get(nodeId) || new Set();
neighbors.forEach(neighbor => this.traverse(neighbor, callback, visited));
}
// Compute hashes for assets to detect drift
async auditAssetHashes(): Promise<void> {
for (const asset of this.matrix.assets) {
try {
const content = await fs.readFile(asset.path, 'utf-8');
asset.hash = createHash('sha256').update(content).digest('hex');
} catch {
console.warn(`Asset not found: ${asset.path}`);
}
}
}
save(): void {
fs.writeFile('matrix.json', JSON.stringify(this.matrix, null, 2));
}
}
3. CLI Integration
Expose the engine via a CLI for daily use. This integrates into the developer's workflow without leaving the terminal.
import { Command } from 'commander';
import { MatrixEngine } from './engine';
const program = new Command();
program
.name('indie-matrix')
.description('Product Matrix CLI for Indie Developers')
.version('1.0.0');
program
.command('analyze')
.description('Analyze portfolio structure and reuse')
.option('-c, --config <path>', 'Path to matrix config', './matrix.json')
.action(async (opts) => {
const engine = new MatrixEngine(opts.config);
const shared = engine.getSharedAssets();
console.log('π Product Matrix Analysis');
console.log(`Total Products: ${engine.matrix.products.length}`);
console.log(`Total Features: ${engine.matrix.features.length}`);
console.log(`Total Assets: ${engine.matrix.assets.length}`);
console.log('\nπ High-Value Shared Assets:');
shared.slice(0, 5).forEach(s => {
console.log(` - ${s.assetId} (Used in ${s.productCount} products)`);
});
});
program
.command('audit')
.description('Update asset hashes and validate integrity')
.option('-c, --config <path>', 'Path to matrix config', './matrix.json')
.action(async (opts) => {
const engine = new MatrixEngine(opts.config);
await engine.auditAssetHashes();
engine.save();
console.log('β
Audit complete. Hashes updated.');
});
program.parse(process.argv);
Rationale
- TypeScript: Chosen for type safety and ecosystem compatibility. Most indie stacks involve TS/JS, allowing seamless integration.
- Zod Validation: Prevents configuration drift. A broken matrix config is caught immediately during parsing, not during runtime analysis.
- Graph Traversal: Enables complex queries like "What breaks if I remove this asset?" or "Which products share this auth implementation?"
- Hashing: Ensures that "shared" assets are actually identical. If hashes diverge, the system flags a fork, prompting the developer to reconcile or explicitly version the asset.
Pitfall Guide
Implementing a Product Matrix introduces new failure modes. Avoid these common mistakes based on production experience.
1. Over-Engineering the Schema
Mistake: Defining 50 fields per product including marketing channels, ad spend, and user demographics.
Impact: The matrix becomes a burden to maintain. Developers stop updating it, leading to stale data.
Best Practice: Limit the schema to technical and strategic essentials: ID, status, dependencies, assets, and monetization model. Business metrics belong in analytics tools, not the matrix.
2. Ignoring Circular Dependencies
Mistake: Allowing Product A to depend on a feature in Product B, which depends on Product A.
Impact: Graph resolution fails; build pipelines hang; architectural debt becomes unmanageable.
Best Practice: The engine must detect cycles during buildGraph(). Throw a fatal error if a cycle is found. Enforce a unidirectional dependency flow where shared code lives in a "Core" product or library, not peer-to-peer.
3. The "Excel Trap"
Mistake: Using the matrix to track tasks, bugs, or deadlines.
Impact: The matrix loses its role as a source of truth for structure. It becomes a cluttered project management tool.
Best Practice: Use the matrix for structure, not state. Link to issues, but do not embed issue tracking. The matrix answers "What exists?" and "How does it relate?", not "What are we doing today?".
4. Stale Asset Paths
Mistake: Moving files in the filesystem without updating matrix.json.
Impact: Hash auditing fails; reuse detection reports false negatives; CI pipelines break.
Best Practice: Integrate the audit command into pre-commit hooks. Or, implement a file watcher that auto-updates paths when files move. Never manually edit paths; use CLI commands to refactor.
5. Confusing Features with Products
Mistake: Creating a product node for every minor feature or configuration tweak.
Impact: Graph bloat. The matrix becomes noise. Analysis tools become slow and unreadable.
Best Practice: A product must have a distinct value proposition, deployment target, or monetization strategy. Features are subdivisions. If two items share the same repo and deploy together, they are likely features of one product, not separate products.
6. Neglecting Monetization Tagging
Mistake: Failing to tag assets with their monetization context.
Impact: Inability to assess which assets drive revenue versus which are cost centers.
Best Practice: Tag features with monetization. The engine can then generate reports showing "Revenue-Generating Assets" vs. "Support Assets." This helps prioritize refactoring efforts on high-value code.
7. Manual Matrix Updates Only
Mistake: Relying on developers to remember to update the matrix when shipping code.
Impact: Matrix drift. The tool becomes useless within weeks.
Best Practice: Automate. Use git hooks to auto-detect new files and suggest matrix entries. Or, generate matrix entries from package.json or manifest files where possible. The matrix should require minimal manual intervention.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Solo Dev, < 3 Projects | Lightweight JSON + CLI | Low overhead; prevents early fragmentation; fast setup. | Minimal dev time; high long-term savings. |
| Portfolio > 5 Projects | Matrix Engine + Plugin Ecosystem | Complex dependencies require graph resolution; automation essential. | Initial setup cost; reduces maintenance by ~50%. |
| Team of 2-3 Indie Devs | Matrix + Shared Repo + PR Reviews | Ensures alignment; prevents divergent asset forks; enforces standards. | Coordination overhead; improves code quality and reuse. |
| Micro-SaaS Focus | Matrix with Monetization Plugins | Prioritizes revenue-generating features; tracks cost-per-feature. | Enables data-driven pruning of low-ROI products. |
| Game Dev / Creative | Matrix with Asset Hashing & Bundling | Manages large binary assets; detects duplicate textures/sounds. | Reduces build sizes; optimizes storage costs. |
Configuration Template
Copy this matrix.json to your repository root.
{
"version": "1.0",
"products": [
{
"id": "prod-001",
"name": "IndieDash",
"description": "Analytics dashboard for indie devs",
"features": ["feat-auth", "feat-metrics"],
"sharedAssets": ["asset-ui-kit", "asset-auth-lib"],
"techStack": ["react", "node", "postgres"],
"monetization": "freemium"
}
],
"features": [
{
"id": "feat-auth",
"name": "Authentication System",
"status": "shipped",
"dependencies": ["asset-auth-lib"],
"monetization": "paid"
},
{
"id": "feat-metrics",
"name": "Real-time Metrics",
"status": "wip",
"dependencies": ["asset-ui-kit"],
"monetization": "free"
}
],
"assets": [
{
"id": "asset-auth-lib",
"name": "Auth Library",
"type": "code",
"path": "libs/auth/index.ts",
"license": "MIT"
},
{
"id": "asset-ui-kit",
"name": "UI Components",
"type": "code",
"path": "libs/ui/index.ts",
"license": "MIT"
}
],
"metadata": {
"lastAudit": "2023-10-27T10:00:00Z"
}
}
Quick Start Guide
- Initialize: Run
npx @codcompass/matrix init in your project root. This generates the default matrix.json and installs dependencies.
- Add Product: Use
npx @codcompass/matrix add product --name "MyApp" --stack "nextjs,tailwind".
- Scan Assets: Run
npx @codcompass/matrix scan. The tool detects local files and suggests asset entries based on patterns.
- Verify: Execute
npx @codcompass/matrix analyze. Review the output for immediate insights and commit the updated matrix.
The Product Matrix transforms indie development from a chaotic collection of projects into a manageable, reusable asset portfolio. By implementing this technical framework, you gain visibility, reduce waste, and accelerate shipping velocity without sacrificing the agility that defines indie success.