$ cd ../projects

~/projects/tala/README.md

LIVEPRIVATE# Full-Stack# AI Automation

TALA

TALA is a self-hosted work hub. It replaced obligations scattered across chat, Google Tasks, email and two internal apps with one Postgres-backed source of truth. It flags stale staff tasks, turns spoken notes into reviewed tasks, mirrors my own tasks to Google Tasks, and runs an email loop where staff reply with photo proof before a task is closed.

Next.js 16React 19TypeScriptPostgreSQLn8nfaster-whisperClaudeCloudflare AccessDocker
end-to-end checks
64
schema migrations
6
stale-task threshold
3 days
AI writes human-approved
100%

cat architecture.flow

System Architecture

architecture.flow

Capture

Browser mic / Telegram
Local Whisper (STT)
n8n · Parse Note
Review card
PostgreSQL

Staff loop

Weekday 08:00 cron
Mail outbox
n8n SMTP sender
Staff reply + photo
Accept / send back

Access

Cloudflare Access
JWT re-verified in proxy
Next.js route handlers
Machine API (Docker network only)

cat problem.md

Business Problem

Work obligations lived in too many places: chat threads, Google Tasks, email, the Liquidation Tracker and Project ATLAS. Nothing showed what I owed, what my staff owed, and what was waiting on someone else.

Delegated tasks had no notion of "last update". A task assigned on Monday could go quiet for a week before anyone noticed, and following up depended on memory.

Capturing work while away from a keyboard (in the field, in a meeting, driving) meant losing it or typing it in later.

cat solution.md

Solution

I built TALA as a single-user web app with PostgreSQL as the one source of truth. Every task, follow-up, project and deadline lives there. A task assigned to staff that has had no update for three days is automatically flagged as stale.

Voice capture records up to two minutes in the browser and transcribes it on a local faster-whisper service, so audio never leaves the homelab. A purpose-built n8n workflow then turns the transcript into proposed tasks. It resolves staff names ("ask the technician to…") and leaves any ambiguous name unassigned instead of guessing. Nothing is created until I approve the proposed tasks on a review card.

Staff follow-ups run as an email loop. A weekday-morning job queues one digest per person, n8n sends it, and staff reply with photo proof. A reply can never close a task. Only my explicit Accept does.

A read-only "Pendings" panel shows what is waiting in the Liquidation Tracker and ATLAS, mapped to who it is waiting on, so I don't have to open three apps every morning.

ls ./components

Architecture Decisions

Next.js route handlers, no separate API

One Next.js 16 app serves the UI and talks to PostgreSQL directly through route handlers. That keeps a single-user tool to one container and one deploy.

Defense-in-depth auth

Cloudflare Access gates the hostname with Google sign-in. The app's proxy re-verifies the Access JWT's issuer, audience and email, and fails closed if any config is missing.

Machine API for automation

n8n talks to TALA through an internal API protected by a bearer token compared in constant time. Requests arriving through the public tunnel are rejected outright, so it is reachable only on the Docker network.

Purpose-built LLM workflows

A generic "run any prompt" relay was rejected as an arbitrary-execution surface. Each LLM job is its own n8n workflow with a fixed prompt. TALA sends only quoted text and staff names, and re-validates everything that comes back.

Local speech-to-text

A faster-whisper service runs on CPU inside the homelab with a vocabulary prompt for mixed English and Tagalog. It handles one request at a time and returns 429 when busy, so it can't starve other workloads.

Migrations and backups

Six numbered SQL migrations are tracked in a schema_migrations table. The database runs under its own least-privilege role and is included in the nightly backup. Proof photos are stored in Postgres so they are backed up too.

./run --trace

Voice Note → Reviewed Tasks

  1. 01

    Record

    MediaRecorder captures up to two minutes of audio in the browser.

  2. 02

    Transcribe locally

    Audio is posted to the local Whisper service. Nothing goes to a cloud speech API.

  3. 03

    Parse

    The Parse Note workflow runs a fixed prompt through Claude with the current staff list, and returns structured task proposals.

  4. 04

    Validate

    TALA re-validates the JSON and matches names. Ambiguous assignees stay unassigned rather than guessed.

  5. 05

    Human approval

    A review card shows every proposed task. Nothing is written until I click "Add N tasks".

./run --trace 2

Staff Follow-up & Photo Proof

  1. 01

    Queue digests

    A weekday 08:00 (Manila) job writes one reminder digest per staff member to a mail outbox table.

  2. 02

    Send

    n8n polls the outbox every two minutes and sends over SMTP. TALA decides every recipient and body, and n8n only transports.

  3. 03

    Reply with proof

    Staff reply by email with photos. IMAP intake de-duplicates by Message-ID and checks each image by its magic bytes and size limits.

  4. 04

    Decide

    I review the proof and choose Accept & close or Send back. A reply alone never closes a task.

tail -f incidents.log

Engineering Challenges

Keeping the LLM surface small

Symptom
The fastest path was a generic webhook that would run any prompt through Claude for any caller.
Root cause
A generic relay is effectively remote code execution on the homelab's Claude CLI.
Fix
Replaced it with purpose-built workflows. The prompt lives in n8n, the app can only send quoted text and names, and the reply is re-validated before use.

Emailing staff without risk

Symptom
An automation bug in the staff email loop could spam real people or close tasks on a bad reply.
Fix
Shipped in PREVIEW mode, where every message goes only to me. One pure function decides delivery, and the e2e suite refuses to run unless preview mode is on. Replies are de-duplicated by Message-ID and can never close a task by themselves.

Unit tests passed, real mail failed

Symptom
Parsed staff replies came through empty in production.
Root cause
The IMAP node exposes the body as `text`, not `textPlain`, and Outlook inserts [cid:] placeholders for inline images.
Fix
Fixed the field mapping and stripped the placeholders. A real mailbox round trip is now part of verification, not just mocks.

Docker builds hanging on the homelab

Symptom
npm installs inside docker build stalled intermittently.
Root cause
The container network MTU (1500) was larger than the host's uplink MTU (1400), so large packets were silently dropped.
Fix
Builds run on the host network. The same root cause was later found and fixed for other services' outbound calls.

cat package.json | jq .stack

Technology Stack

Application

Next.js 16React 19TypeScriptTailwind CSS 4jose

Data

PostgreSQL 16pgvectorSQL migrationsNightly dumps

Automation & AI

n8nClaude CLIfaster-whisperGmail SMTP / IMAPGoogle Tasks

Platform & Security

DockerCloudflare TunnelCloudflare AccessNode test runner

echo $EXIT_STATUS

Project Outcome

outcome.log

TALA is my daily driver. Every task I own and every follow-up I delegate is in one place, stale work surfaces on its own, and spoken notes become reviewed tasks in under a minute.

The project is also a deliberate exercise in safe AI automation. The LLM has a narrow, fixed job; a human approves every write; outbound email is gated behind a preview mode the tests enforce; and every external integration was verified against the real service, not just mocks.