Tech Specs
Personal portfolio and AI-native sandbox for Ricardo Fernández Gasca. Last updated: March 1, 2026.
1. Stack Overview
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.1.6 |
| Language | TypeScript | ^5 |
| UI Library | React | 19.2.3 |
| Styling | Tailwind CSS v4 | ^4 |
| Animations | Framer Motion | ^12 |
| Fonts | Inter + JetBrains Mono | Google Fonts (via next/font) |
| MDX Rendering | next-mdx-remote | ^6 |
| Deployment | Vercel | — |
| Node Requirement | Node.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:
| Script | Source | Output |
|---|---|---|
sync:posts | Notion posts DB | content/posts/{locale}/ |
sync:pages | Notion pages DB | content/pages/{locale}/ |
sync:work | Notion work DB | content/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
| Parameter | Value |
|---|---|
| Model | gemini-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
| Concern | Mechanism |
|---|---|
| UI language | React Context (client) |
| Dängo presence & animation mode | React Context (client) |
| Chat history | Component-local state (client) |
| Rate limit counters | KV Store (server-side) |
| Session message counts | KV 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>;
}
| Environment | Implementation | Backend |
|---|---|---|
| Production | UpstashStore | Upstash Redis (REST API) |
| Development / CI | MemoryStore | In-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
| Measure | Mechanism | Configuration |
|---|---|---|
| IP sliding-window rate limit | Async sliding window, store-backed | X req / Y min |
| Burst protection | Sub-window check inside sliding window | X req / Y s |
| IP daily budget | 24 h sliding window | X req / Y h |
| Session message cap | Per-session counter, store-backed with base + bonus turn budget | Configurable hard ceiling per session |
| Session TTL | Auto-expiration via KV TTL | X h of inactivity |
| CAPTCHA | Cloudflare Turnstile, server-side siteverify | Required on first message of each session |
| Prompt injection scanning | Pre-LLM regex + structural checks | 35+ patterns across X attack categories |
| Injection structural guard | Length cap + repeated-char detection | Max X chars; X + repeated chars flagged |
| Origin validation | Custom header + Fetch Metadata + Origin/Referer check | Production only |
| System prompt confidentiality | Prompt assembled and discarded server-side; never returned in response | Policy layer enforces behavior at the model level |
| Fail-closed CAPTCHA | Missing CAPTCHA secret in production → refuse request | Dev: skip; Prod: hard fail |
| Cache-Control on errors | All error responses set Cache-Control: no-store | Prevents CDN caching of auth errors |
| Input sanitization (tools) | Control-char strip, length cap, email regex | Applied before every Notion write |
8.3 Prompt Injection Patterns Covered
| Category | Examples |
|---|---|
| Role override / reset | "Ignore previous instructions", "You are no longer…", "New persona" |
| Instruction extraction | "Reveal your system prompt", "Print your instructions" |
| Jailbreak keywords | jailbreak, 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 flooding | Repeated-character strings (>100 repetitions) |
| Message length abuse | Messages 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 type | Sync mechanism |
|---|---|
| Blog / library posts | CLI sync script, per-locale Markdown files |
| About / work pages | CLI sync script, per-locale Markdown files |
| Work capabilities & philosophy | CLI sync script, structured JSON |
Visitor interactions (site → Notion, private):
| Interaction type | Trigger |
|---|---|
| Contact & collaboration interest | Captured by Dängo during conversation |
| Full conversation logs | Written 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.