Tech Specs

Personal portfolio and AI-native sandbox for Ricardo Fernández Gasca. Last updated: October 9, 2026..


1. Stack Overview

LayerTechnologyVersion
FrameworkNext.js (App Router)16.4.0
LanguageTypeScript^5
UI LibraryReact19.2.3
StylingTailwind CSS v4^4
AnimationsFramer Motion^12
FontsInter + JetBrains MonoGoogle Fonts (via next/font)
MDX Renderingnext-mdx-remote^6
DeploymentVercel—
Node RequirementNode.js≥ 22.x
TestingVitestpure-module suite
CIGitHub Actionsevery pull request

Every pull request runs an automated check — lint, type-check, a Vitest suite covering the pure logic (permission matrices, date and aggregation math, validators) and a full production build — and must pass it, plus an automated code review, before it can be merged. Code changes are merged by hand once both pass.


2. Architecture

┌─────────────────────────────────────────────────────────┐
│                      Next.js App Router                  │
│                                                          │
│   ┌──────────┐  ┌──────────┐  ┌──────────────────────┐  │
│   │  Pages   │  │ API Routes│  │  Static / MDX Content │  │
│   │ /        │  │ /api/chat │  │  Notion → sync scripts│  │
│   │ /library │  │ /api/chat/│  │  → .md files          │  │
│   │ /work    │  │  conv.    │  │  → gray-matter parse  │  │
│   │ /chat    │  │           │  │                        │  │
│   │ /contact │  │ /api/contact│ │                        │  │
│   └──────────┘  └──────────┘  └──────────────────────┘  │
└─────────────────────────────────────────────────────────┘
         │                   │
         ▼                   ▼
  Upstash Redis        Google Gemini API
  (rate limits,        (Dango streaming
   sessions, KV)        + tool calls)
         │
         ▼
     Notion API
  (visitor data +
  conversation logs)

Routing

  • Static content (library, work, writing) compiles at build time from Notion-synced MDX.
  • Dynamic chat (/api/chat) is forced dynamic (export const dynamic = "force-dynamic") to prevent response buffering.
  • Redirects in next.config.ts: /writing → /library, /interests → /library.

3. Content System

Content is authored in Notion and synced via three CLI scripts:

ScriptSourceOutput
sync:postsNotion posts DBcontent/posts/{locale}/
sync:pagesNotion pages DBcontent/pages/{locale}/
sync:workNotion work DBcontent/work/*.json

Markdown frontmatter is extracted with gray-matter. MDX rendering uses next-mdx-remote. Locales supported: en, es, fr.


4. Internationalization

  • Three locales: English, Spanish, French.
  • Client-side language selection via React Context.
  • Translation strings centralized.
  • AI assistant (Dango) auto-detects visitor language from the active interface locale and adapts mid-conversation.

5. Dängo — AI Assistant

Dängo is the site's native conversational AI agent. Not a chatbot widget, not a form with a personality — a purpose-built streaming agent with a multi-layered identity, live tool-calling, visitor interaction management, and a defense-in-depth security model baked in at every layer.

The name comes from grandeza in Otomí. It's not aesthetic. It's identity.

5.1 Model & Capacity

ParameterValue
Modelgemini-3.8-flash
Max output tokens~750 words
Max conversation context~5 000 tokens

5.2 Identity Architecture

Dängo's behavior is not a single prompt. It is assembled server-side on every request from layered instruction blocks, combining live context, personality, policy, and behavioral rules:

┌─────────────────────────────┐
│  Datetime hint (injected)   │  ← Current UTC datetime, always fresh
├─────────────────────────────┤
│  Locale hint (injected)     │  ← Active interface language
├─────────────────────────────┤
│  Identity layer             │  ← Personality, tone, cultural context
├─────────────────────────────┤
│  Policy layer               │  ← Hard boundaries and refusal logic
├─────────────────────────────┤
│  Operational layer          │  ← How and when to use tools
└─────────────────────────────┘

The full assembled instruction set is never included in the HTTP response or exposed to the client in any form.

5.3 Tool System

Dängo can call server-side tools mid-stream. Tools are executed transparently — the visitor never sees the mechanism, only the result woven into the response.

Available capabilities include:

  • Retrieving published content from the site to answer questions accurately
  • Capturing visitor contact information and intent when someone expresses genuine interest

All tool inputs are sanitized (control character stripping, length capping, format validation) before any write operation.

5.4 Language Adaptation

Dängo defaults to Spanish. If the visitor's interface is set to English or French, the locale hint is injected at the top of the instruction set and Dängo opens in that language. Mid-conversation language switches are also detected and matched — no configuration needed by the visitor.

5.5 Streaming & Tool Calls

Responses stream token-by-token via Server-Sent Events. If a tool call is triggered mid-stream, the stream pauses, the tool executes server-side, and generation resumes — the entire cycle is invisible to the client. There is no polling, no loading state between tool execution and response.

5.6 Conversation Logging

Every session is logged on close. Close triggers include: session limit reached, visitor navigates away, or a contact interaction is completed. Each log captures:

  • Session identifier and timestamp
  • Close reason
  • Full message history
  • All tool invocations with their inputs and outputs
  • Model reasoning traces (when emitted during generation)

Logs are stored in a private Notion workspace, not accessible from the public site.


6. State Management

ConcernMechanism
UI languageReact Context (client)
Dängo presence & animation modeReact Context (client)
Chat historyComponent-local state (client)
Rate limit countersKV Store (server-side)
Session message countsKV Store (server-side)

Dängo's client-side presence cycles through four animation states: idle, thinking (waiting for first token), streaming (tokens arriving), and navigating (animating toward a page transition).


7. KV Store Abstraction

All server-side stateful operations (rate limits, sessions) go through a KVStore interface:

interface KVStore {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
  del(key: string): Promise<void>;
}
EnvironmentImplementationBackend
ProductionUpstashStoreUpstash Redis (REST API)
Development / CIMemoryStoreIn-process Map

The backend is selected at module load time based on the presence of Redis connection credentials in the environment. Swapping stores requires no changes to security or session code. The in-memory store auto-prunes expired entries every 2 minutes and uses unref() to avoid blocking serverless process exit.


8. Security

8.1 Request Pipeline

Every POST /api/chat request traverses eight ordered gates before reaching the model:

1. JSON parse & shape validation
2. Message budget validation (character counts)
3. IP rate limit          ← fail fast
4. IP daily limit         ← closes session-rotation loophole
5. Origin validation      ← blocks curl/bot requests
6. Turnstile CAPTCHA      ← first message of new sessions only
7. Session message limit  ← per-session cap with TTL
8. Injection scan         ← latest user message scanned pre-LLM

8.2 Security Measures Table

MeasureMechanismConfiguration
IP sliding-window rate limitAsync sliding window, store-backedX req / Y min
Burst protectionSub-window check inside sliding windowX req / Y s
IP daily budget24 h sliding windowX req / Y h
Session message capPer-session counter, store-backed with base + bonus turn budgetConfigurable hard ceiling per session
Session TTLAuto-expiration via KV TTLX h of inactivity
CAPTCHACloudflare Turnstile, server-side siteverifyRequired on first message of each session
Prompt injection scanningPre-LLM regex + structural checks35+ patterns across X attack categories
Injection structural guardLength cap + repeated-char detectionMax X chars; X + repeated chars flagged
Origin validationCustom header + Fetch Metadata + Origin/Referer checkProduction only
System prompt confidentialityPrompt assembled and discarded server-side; never returned in responsePolicy layer enforces behavior at the model level
Fail-closed CAPTCHAMissing CAPTCHA secret in production → refuse requestDev: skip; Prod: hard fail
Cache-Control on errorsAll error responses set Cache-Control: no-storePrevents CDN caching of auth errors
Input sanitization (tools)Control-char strip, length cap, email regexApplied before every Notion write

8.3 Prompt Injection Patterns Covered

CategoryExamples
Role override / reset"Ignore previous instructions", "You are no longer…", "New persona"
Instruction extraction"Reveal your system prompt", "Print your instructions"
Jailbreak keywordsjailbreak, DAN mode, developer mode, god mode, unrestricted mode
Bypass/override framing"Bypass your filters", "Disable your safety", "Override your rules"
Simulation attacks"Simulate a different AI", "Act as if you are a different model"
Context floodingRepeated-character strings (>100 repetitions)
Message length abuseMessages exceeding 15 000 characters

9. Notion Integration

Notion serves a dual role: content management system for public-facing content, and private backend for visitor interaction data.

Content (Notion → site):

Content typeSync mechanism
Blog / library postsCLI sync script, per-locale Markdown files
About / work pagesCLI sync script, per-locale Markdown files
Work capabilities & philosophyCLI sync script, structured JSON

Visitor interactions (site → Notion, private):

Interaction typeTrigger
Contact & collaboration interestCaptured by Dängo during conversation
Full conversation logsWritten automatically on session close

No visitor data is ever surfaced back to the public site. All write operations go to private Notion workspaces inaccessible from the front-end.


10. External Service Credentials

The application requires credentials for five external services, injected at runtime via environment configuration. No secrets are bundled into the build artifact.

11. The Private Client — /dango

Everything documented above is public. Behind a login there is a second half of the site: the same Dängo, plus persistence, plus two products that are not chat at all.

11.1 Authentication

Identity is handled by Supabase Auth — email and password, with sessions issued and refreshed through server-side cookies. Registration is invite-only: an admin generates a single-use link, the visitor opens it, picks a username and password, and is signed in immediately. The token cannot be reused.

11.2 What an Account Adds

CapabilityBehaviour
Conversation historyEvery past conversation is stored, expandable, and can be continued — prior turns reload as context
Long-term memoryAfter a session closes, Dängo extracts a handful of facts about you, embeds them, and retrieves the relevant ones at the start of the next session
Personalised behaviourWho you are and your relation to me are injected into the instruction stack, and shape Dängo's register
Per-user modelEach account is assigned a model from the catalog
QuotasPer-user message and token limits, enforced server-side before the model is reached; any dimension left unset is unlimited
Tool tiersSome tools are public, some admin-only, and some granted per user

Authenticated sessions skip the CAPTCHA and the anonymous IP limits — they are bounded by the account's own quota instead. Every successful turn writes an immutable usage record.

11.3 Admin

Admins get a management panel over the whole user base: assigned model, quotas, tool and feature grants, conversation counts, monthly token usage, and the invite generator. Changes apply immediately.


12. Workflows — Document Extraction

A pipeline that turns a folder of documents into one reviewable spreadsheet.

Google Drive picker (your own OAuth)
  → download server-side
  → Document AI OCR (inline, no storage bucket)
  → Gemini structured extraction (OCR text + page image, JSON mode)
  → flatten: one document → one or more rows
  → aggregate → CSV / XLSX

Documents are processed in small batches and streamed back over Server-Sent Events, so long jobs report progress. Rows are persisted per document: a dropped connection loses no completed work, and re-running a single file replaces only that file's rows.

Each document type is a use case — a self-contained contract of four parts: the prompt, the typed response schema the model must return, how one document flattens into rows, and the output columns. Everything else (OCR, batching, export, UI) is shared, so adding a document type adds no pipeline code. Use cases come either from version-controlled code, or from a builder in the admin panel that needs no deploy.

BoundValue
Documents per job200
OCR ceiling per document~20 MB / 15 pages
Drive accessYour own OAuth grant, limited to the files you pick

Source files are read with your own Drive authorisation and are never copied into a storage bucket — only the extracted rows are kept. Access is granted per document type.


13. Food — Meal Planning

A household meal-planning module: a recipe library, weekly plans, shopping lists, and a conversational kitchen assistant that writes recipes from a photo, a PDF, a pasted block of text, or just an idea. Private tooling — the pages are not indexed.

Access has two independent axes, and keeping them separate is the point: a grant decides whether you can open the module at all, and your role inside a household (owner, editor, shopper, viewer) decides what you can do there. A single role→action matrix is shared by the server and the UI so the two cannot drift. Being a site admin does not make you a member of someone's household.

Shopping lists can be shared through a public, unindexed link, so whoever is actually at the shop doesn't need an account.


14. Access — and How to Ask for It

Open to everyone, no account: the public pages, this spec, and /chat — Dängo with its full tool set, rate-limited and CAPTCHA-gated but not behind a login.

Behind an invite: sections 11, 12 and 13. Registration is invite-only by design, not by oversight — every authenticated feature spends tokens against a real budget, and long-term memory means the system keeps notes on the people who use it. Both are reasons to know who is on the other side.

Access is layered rather than all-or-nothing:

SurfaceRequires
Public site, /chatNothing
/dango — chat, history, memoryAn account (invite)
A workflow document typeAn account + that workflow's grant
FoodAn account + the Food grant + a household membership

Want in? Say what you'd use it for — a workflow for a document type you deal with, a household to plan meals for, or plain curiosity about the memory layer.

→ Request access — one click, opens the contact form with the request already written.