Tech Specs

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


1. Stack Overview

LayerTechnologyVersion
FrameworkNext.js (App Router)16.1.6
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≥ 20

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: enesfr.


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-flash-preview
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 keywordsjailbreakDAN modedeveloper modegod modeunrestricted 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.