Files
simplenote-web/ARCHITECTURE.md
Bulma 90e4dd0807 feat(architecture): add complete technical architecture for SimpleNote
- ARCHITECTURE.md: main architecture document
- api-spec.yaml: full OpenAPI 3.0 spec
- folder-structure.md: detailed folder layout
- data-format.md: JSON schemas for .meta.json, .library.json, .tag-index.json
- env-template.md: environment variables documentation
- cli-protocol.md: CLI-to-API communication protocol
2026-03-28 03:18:25 +00:00

11 KiB

SimpleNote - Arquitectura Técnica

1. Visión General

Sistema de gestión de documentos basado en archivos Markdown con API REST. Diseñado para reemplazar Joplin con una arquitectura más simple.

┌─────────────────┐      ┌──────────────────┐
│  simplenote-cli │◄────►│  simplenote-web   │
│  (Commander.js) │     │  (Express + API)  │
└─────────────────┘      └────────┬─────────┘
                                  │
                         ┌────────▼─────────┐
                         │   File System     │
                         │  (Markdown + JSON)│
                         └───────────────────┘

2. Componentes

2.1 simplenote-web

  • Rol: API REST + Frontend web
  • Puerto default: 3000
  • Estructura interna: Express.js con routers modulares

2.2 simplenote-cli

  • Rol: Cliente de línea de comandos
  • Conexión: HTTP al API de simplenote-web
  • Config: ~/.config/simplenote/config.json

3. Arquitectura de Datos

3.1 Estructura de Librerías (Filesystem)

data/                                  # DATA_ROOT configurable
└── libraries/
    └── {library-id}/
        ├── .library.json              # Metadata de librería
        ├── documents/
        │   └── {document-id}/
        │       ├── index.md           # Contenido
        │       └── .meta.json         # Metadata del documento
        └── sub-libraries/
            └── {child-lib-id}/...     # Anidamiento recursivo

3.2 JSON Manifests

.library.json — Metadata de librería:

{
  "id": "uuid-v4",
  "name": "Nombre de Librería",
  "parentId": "parent-uuid | null",
  "path": "/libraries/uuid",
  "createdAt": "ISO8601",
  "updatedAt": "ISO8601"
}

.meta.json — Metadata de documento:

{
  "id": "uuid-v4",
  "title": "string",
  "tags": ["tag1", "tag2"],
  "type": "requirement|note|spec|general",
  "status": "draft|approved|implemented",
  "priority": "high|medium|low",
  "createdBy": "agent-id",
  "createdAt": "ISO8601",
  "updatedAt": "ISO8601",
  "libraryId": "uuid"
}

.tag-index.json — Índice global de tags (en DATA_ROOT):

{
  "version": 1,
  "updatedAt": "ISO8601",
  "tags": {
    "backend": ["doc-1", "doc-2"],
    "api": ["doc-1", "doc-3"],
    "auth": ["doc-2"]
  }
}

3.3 Formato de Documento (Markdown)

---
id: REQ-001
title: Título del Requerimiento
type: requirement
priority: high
status: draft
tags: [backend, api]
createdBy: agent-id
createdAt: 2026-03-28
---

# Título

## Descripción
Descripción del requerimiento.

## Criterios de Aceptación
- [ ] Criterio 1
- [ ] Criterio 2

4. API REST

4.1 Base URL

/api/v1

4.2 Autenticación

  • Método: Token Bearer en header Authorization
  • Formato: Authorization: Bearer <token>
  • Admin tokens: Generados en setup inicial, almacenados en .auth-tokens.json

4.3 Endpoints

Auth

Method Endpoint Descripción
POST /api/v1/auth/token Generar token (admin)
GET /api/v1/auth/verify Verificar token válido

Documents

Method Endpoint Descripción
GET /api/v1/documents Listar (filtros: tag, library, type)
GET /api/v1/documents/:id Obtener documento + metadata
POST /api/v1/documents Crear documento
PUT /api/v1/documents/:id Actualizar documento
DELETE /api/v1/documents/:id Eliminar documento
GET /api/v1/documents/:id/export Exportar como Markdown

Libraries

Method Endpoint Descripción
GET /api/v1/libraries Listar librerías raíz
GET /api/v1/libraries/:id Ver contenido de librería
POST /api/v1/libraries Crear librería
GET /api/v1/libraries/:id/tree Árbol completo de sublibrerías
DELETE /api/v1/libraries/:id Eliminar librería

Tags

Method Endpoint Descripción
GET /api/v1/tags Listar todos los tags
GET /api/v1/tags/:tag/documents Docs con tag específico
POST /api/v1/documents/:id/tags Agregar tags a documento

5. Middleware de Auth

5.1 Flujo de Tokens

  1. Setup inicial: Se genera admin token (almacenado en .auth-tokens.json)
  2. CLI login: simplenote auth login <token> → almacena token en config local
  3. Requests: Token enviado en header Authorization: Bearer <token>
  4. Verificación: Middleware busca token en .auth-tokens.json

5.2 Implementación

// authMiddleware.js
async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'Token required' });
  }

  const tokens = await readJSON('.auth-tokens.json');
  if (!tokens.valid.includes(token)) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  req.token = token;
  next();
}

5.3 Archivo .auth-tokens.json

{
  "version": 1,
  "tokens": [
    {
      "token": "snk_xxxxx",
      "label": "cli-default",
      "createdAt": "ISO8601"
    }
  ]
}

6. Estrategia de Indexación de Tags

6.1write-through

Cuando se crea/actualiza/elimina un documento:

  1. Se actualiza su .meta.json con los tags
  2. Se reconstruye el .tag-index.json global

6.2 Optimización

  • El índice se rebuild en memoria al iniciar (rápido para <10k docs)
  • Para sistemas grandes: rebuild incremental (solo afecta docs modificados)
  • El índice es un archivo JSON plano para búsqueda O(1) por tag

6.3 API de Búsqueda

// GET /api/v1/tags/backend
// → Lee .tag-index.json → returns ["doc-1", "doc-2"]

// GET /api/v1/documents?tag=backend
// → Busca en índice → filtra docs en memoria → retorna

7. CLI API Client

7.1 Configuración Local

// ~/.config/simplenote/config.json
{
  "apiUrl": "http://localhost:3000/api/v1",
  "token": "snk_xxxxx",
  "activeLibrary": "default"
}

7.2 Cliente HTTP

// cli/src/api/client.js
class SimpleNoteClient {
  constructor(baseUrl, token) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  async request(method, path, body) {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json'
      },
      body: body ? JSON.stringify(body) : undefined
    });

    if (!res.ok) {
      const err = await res.json().catch(() => ({ error: res.statusText }));
      throw new Error(err.error || `HTTP ${res.status}`);
    }

    return res.json();
  }

  // Documents
  listDocuments(params) { return this.request('GET', '/documents', null); }
  getDocument(id) { return this.request('GET', `/documents/${id}`); }
  createDocument(data) { return this.request('POST', '/documents', data); }
  updateDocument(id, data) { return this.request('PUT', `/documents/${id}`, data); }
  deleteDocument(id) { return this.request('DELETE', `/documents/${id}`); }

  // Libraries
  listLibraries() { return this.request('GET', '/libraries'); }
  createLibrary(data) { return this.request('POST', '/libraries', data); }

  // Tags
  listTags() { return this.request('GET', '/tags'); }
  getTagDocuments(tag) { return this.request('GET', `/tags/${tag}/documents`); }

  // Auth
  verifyToken() { return this.request('GET', '/auth/verify'); }
}

8. Dependencias NPM

simplenote-web

{
  "dependencies": {
    "express": "^4.18.2",
    "uuid": "^9.0.0",
    "marked": "^11.0.0",
    "gray-matter": "^4.0.3"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  }
}

simplenote-cli

{
  "dependencies": {
    "commander": "^11.1.0",
    "axios": "^1.6.0"
  }
}

9. Variables de Entorno

Variable Default Descripción
PORT 3000 Puerto del servidor
DATA_ROOT ./data Raíz de documentos
HOST 0.0.0.0 Host de binding
LOG_LEVEL info Nivel de logging
CORS_ORIGIN * Orígenes CORS permitidos

10. Estructura de Archivos del Proyecto

simplenote-web/
├── src/
│   ├── index.js              # Entry point
│   ├── app.js                # Express setup
│   ├── routes/
│   │   ├── documents.js
│   │   ├── libraries.js
│   │   ├── tags.js
│   │   └── auth.js
│   ├── services/
│   │   ├── documentService.js
│   │   ├── libraryService.js
│   │   └── tagService.js
│   ├── middleware/
│   │   └── auth.js
│   ├── utils/
│   │   ├── markdown.js
│   │   ├── fsHelper.js
│   │   └── uuid.js
│   └── indexers/
│       └── tagIndexer.js
├── data/                     # .gitkeep
├── tests/
├── package.json
└── README.md

simplenote-cli/
├── src/
│   ├── index.js              # Entry point (Commander)
│   ├── commands/
│   │   ├── doc.js
│   │   ├── lib.js
│   │   └── tag.js
│   ├── api/
│   │   └── client.js
│   └── config/
│       └── loader.js
├── package.json
└── README.md

11. Flujo de Operaciones

11.1 Crear Documento (CLI)

simplenote doc create --title "REQ-001" --tags "backend,api"
  → POST /api/v1/documents
  → Service: genera UUID, crea /data/libraries/{lib}/documents/{id}/index.md
  → Service: crea /data/libraries/{lib}/documents/{id}/.meta.json
  → Service: rebuild .tag-index.json
  → Response: { id, path, tags, ... }

11.2 Crear Documento (Web)

Form POST → /api/v1/documents
  → Mismo flujo que CLI
  → Web reconstruye lista con filtros

11.3 Búsqueda por Tag

GET /api/v1/tags/backend
  → tagIndexer: lee .tag-index.json
  → Returns: ["doc-id-1", "doc-id-2"]

GET /api/v1/documents?tag=backend
  → Busca en índice → filtra docs → retorna con metadata

12. Seguridad

  • Tokens con prefijo snk_ para identificación fácil
  • Tokens almacenados en texto plano (sistema multi-usuario confiado)
  • Para producción futura: hash bcrypt + salt
  • CORS configurable para restringir acceso web

13. Líneas de Código Estimadas

Componente LOC aprox
API REST (Express) ~600
Services (document, library, tag) ~400
Middleware auth ~50
Tag indexer ~100
CLI client ~300
CLI commands ~400
Total ~1850