diff --git a/API.md b/API.md
index 6da93e3..1e0d1cc 100644
--- a/API.md
+++ b/API.md
@@ -113,6 +113,12 @@
- `GET /api/attachments?entityType=activity&entityId=1` · `POST /api/attachments` (multipart)
- `GET /api/attachments/:id/download` · `DELETE /api/attachments/:id`
+### Notas (`/api/notes`)
+- `GET /api/notes` — solo las del usuario autenticado, ordenadas por `updated_at` descendente.
+- `POST /api/notes` — `{ content }`. `PUT /api/notes/:id` — `{ content }`. `DELETE /api/notes/:id`.
+- Siempre privadas: una nota de otro usuario responde `404` (no `403`, para no revelar que
+ existe). Sin restricción de rol — cualquier autenticado tiene las suyas.
+
### Notificaciones (`/api/notifications`)
- `GET /api/notifications` · `PATCH /api/notifications/:id/read` · `PATCH /api/notifications/read-all`
diff --git a/BASE_DE_DATOS.md b/BASE_DE_DATOS.md
index 302f9f8..5a6f393 100644
--- a/BASE_DE_DATOS.md
+++ b/BASE_DE_DATOS.md
@@ -39,6 +39,7 @@
| `notifications` | notificaciones in-app por usuario |
| `audit_log` | auditoría central (quién, qué, sobre qué, cuándo) |
| `error_logs` | errores de backend (excepciones 500) y frontend (JS/promesas sin catch), panel "Errores del sistema" en Configuración |
+| `notes` | apuntes rápidos personales, siempre privados de cada usuario, menú propio "Notas" |
## Relaciones clave
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d99000c..a8cdd5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog - project.linkedcorp.net
+## [1.23.0] - 2026-09-17
+### Added
+- Menú "Notas" nuevo: apuntes rápidos y simples, siempre privados de cada usuario (nadie más los
+ ve). Crear, editar y eliminar, con fecha y hora de creación (y de última edición, si aplica).
+ Sin permisos especiales — cualquier usuario autenticado tiene los suyos.
+- Nuevo `notes` (tabla) + `GET/POST/PUT/DELETE /api/notes`.
+
## [1.22.0] - 2026-09-17
### Added
- La app ahora es instalable desde el navegador (PWA): ícono propio, se abre en su propia ventana
diff --git a/VERSION b/VERSION
index 57807d6..a6c2798 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.22.0
+1.23.0
diff --git a/backend/database/migrations/015_notes.sql b/backend/database/migrations/015_notes.sql
new file mode 100644
index 0000000..325bb75
--- /dev/null
+++ b/backend/database/migrations/015_notes.sql
@@ -0,0 +1,11 @@
+-- Apuntes rapidos personales (menu "Notas"): siempre privados de cada usuario, sin permisos
+-- especiales de rol -- cualquiera autenticado ve/edita/borra solo los suyos.
+CREATE TABLE IF NOT EXISTS notes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ content TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_notes_user (user_id),
+ CONSTRAINT fk_notes_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/backend/package.json b/backend/package.json
index 7b462ae..fae0fb7 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
- "version": "1.22.0",
+ "version": "1.23.0",
"private": true,
"type": "module",
"main": "src/server.js",
diff --git a/backend/src/app.js b/backend/src/app.js
index b7fe516..a1fee32 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -22,6 +22,7 @@ import recurringTasksRoutes from './modules/recurring-tasks/routes.js';
import searchRoutes from './modules/search/routes.js';
import updaterRoutes from './modules/updater/routes.js';
import errorLogsRoutes from './modules/errorLogs/routes.js';
+import notesRoutes from './modules/notes/routes.js';
export const app = express();
@@ -48,6 +49,7 @@ app.use('/api/recurring-tasks', recurringTasksRoutes);
app.use('/api/search', searchRoutes);
app.use('/api/updater', updaterRoutes);
app.use('/api/error-logs', errorLogsRoutes);
+app.use('/api/notes', notesRoutes);
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
diff --git a/backend/src/modules/notes/controller.js b/backend/src/modules/notes/controller.js
new file mode 100644
index 0000000..8bf6edf
--- /dev/null
+++ b/backend/src/modules/notes/controller.js
@@ -0,0 +1,19 @@
+import { ok, asyncHandler } from '../../utils/http.js';
+import * as service from './service.js';
+
+export const getNotes = asyncHandler(async (req, res) => {
+ ok(res, await service.listNotes(req.currentUser));
+});
+
+export const postNote = asyncHandler(async (req, res) => {
+ ok(res, await service.createNote(req.body.content, req.currentUser), 201);
+});
+
+export const putNote = asyncHandler(async (req, res) => {
+ ok(res, await service.updateNote(Number(req.params.id), req.body.content, req.currentUser));
+});
+
+export const deleteNote = asyncHandler(async (req, res) => {
+ await service.removeNote(Number(req.params.id), req.currentUser);
+ ok(res, { deleted: true });
+});
diff --git a/backend/src/modules/notes/repository.js b/backend/src/modules/notes/repository.js
new file mode 100644
index 0000000..5eb8b30
--- /dev/null
+++ b/backend/src/modules/notes/repository.js
@@ -0,0 +1,28 @@
+import { pool } from '../../config/db.js';
+
+export async function findByUser(userId) {
+ const [rows] = await pool.query(
+ 'SELECT id, user_id, content, created_at, updated_at FROM notes WHERE user_id = ? ORDER BY updated_at DESC',
+ [userId]
+ );
+ return rows;
+}
+
+export async function findById(id) {
+ const [rows] = await pool.query('SELECT id, user_id, content, created_at, updated_at FROM notes WHERE id = ?', [id]);
+ return rows[0] || null;
+}
+
+export async function create(userId, content) {
+ const [result] = await pool.query('INSERT INTO notes (user_id, content) VALUES (?, ?)', [userId, content]);
+ return findById(result.insertId);
+}
+
+export async function update(id, content) {
+ await pool.query('UPDATE notes SET content = ? WHERE id = ?', [content, id]);
+ return findById(id);
+}
+
+export async function remove(id) {
+ await pool.query('DELETE FROM notes WHERE id = ?', [id]);
+}
diff --git a/backend/src/modules/notes/routes.js b/backend/src/modules/notes/routes.js
new file mode 100644
index 0000000..0ba398f
--- /dev/null
+++ b/backend/src/modules/notes/routes.js
@@ -0,0 +1,14 @@
+import { Router } from 'express';
+import { requireAuth } from '../../middleware/auth.js';
+import { getNotes, postNote, putNote, deleteNote } from './controller.js';
+
+const router = Router();
+
+// Sin restriccion de rol: cualquier usuario autenticado tiene sus propias notas.
+router.use(requireAuth);
+router.get('/', getNotes);
+router.post('/', postNote);
+router.put('/:id', putNote);
+router.delete('/:id', deleteNote);
+
+export default router;
diff --git a/backend/src/modules/notes/service.js b/backend/src/modules/notes/service.js
new file mode 100644
index 0000000..b2f8eb4
--- /dev/null
+++ b/backend/src/modules/notes/service.js
@@ -0,0 +1,30 @@
+import * as repo from './repository.js';
+import { HttpError } from '../../utils/http.js';
+
+// Siempre privadas: cada quien ve y toca solo sus propias notas, sin importar el rol. Una nota
+// de otro usuario responde 404 (no 403) para no revelar que existe.
+async function getOwnNoteOr404(id, currentUser) {
+ const note = await repo.findById(id);
+ if (!note || note.user_id !== currentUser.id) throw new HttpError(404, 'Nota no encontrada');
+ return note;
+}
+
+export const listNotes = (currentUser) => repo.findByUser(currentUser.id);
+
+export async function createNote(content, currentUser) {
+ const trimmed = (content || '').trim();
+ if (!trimmed) throw new HttpError(400, 'El contenido es obligatorio');
+ return repo.create(currentUser.id, trimmed);
+}
+
+export async function updateNote(id, content, currentUser) {
+ await getOwnNoteOr404(id, currentUser);
+ const trimmed = (content || '').trim();
+ if (!trimmed) throw new HttpError(400, 'El contenido es obligatorio');
+ return repo.update(id, trimmed);
+}
+
+export async function removeNote(id, currentUser) {
+ await getOwnNoteOr404(id, currentUser);
+ await repo.remove(id);
+}
diff --git a/frontend/package.json b/frontend/package.json
index d233695..5d2e762 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "project-linkedcorp-frontend",
"private": true,
- "version": "1.22.0",
+ "version": "1.23.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/app/App.jsx b/frontend/src/app/App.jsx
index 1806a9a..8c13615 100644
--- a/frontend/src/app/App.jsx
+++ b/frontend/src/app/App.jsx
@@ -19,6 +19,7 @@ import ProyectosPage from '../pages/Proyectos/ProyectosPage.jsx';
import ProyectoDetallePage from '../pages/Proyectos/ProyectoDetallePage.jsx';
import ConfiguracionPage from '../pages/Configuracion/ConfiguracionPage.jsx';
import RecurringTasksPage from '../pages/RecurringTasks/RecurringTasksPage.jsx';
+import NotasPage from '../pages/Notas/NotasPage.jsx';
export default function App() {
return (
@@ -39,6 +40,7 @@ export default function App() {