๐ŸŠ Imprint โ€” Capture at the Speed of Thought

Live: imprint.iampranesh.me ยท Code: github.com/pranesh25joshi/typescript-note-agent


๐Ÿ’ก Inspiration

Every note-taking app I used made me do the paperwork of note-taking: open the right note, type into the right field, tick the right box, archive the stale ones. I kept losing good ideas to the friction of organizing them. So I wanted the inverse โ€” a notebook where I just talk, and software does the filing. That became Imprint: an AI agent that owns the CRUD work (create, edit, complete, delete, find notes) while I stay in conversation.

๐Ÿ› ๏ธ How I built it

  • Frontend โ€” React 19 + Vite + TypeScript, fully client-side, styled with hand-written CSS (no UI kit). Masonry "bento" note grid, inline pencil-editing on every card, toasts, skeleton loading, and voice dictation through the Web Speech API.
  • Backend โ€” Express 5 + TypeScript, Prisma 6 with the Postgres driver adapter, JWT auth using rotating httpOnly cookies, and zod-validated routes.
  • The agent โ€” Gemini 2.5 Flash (via the OpenAI-compatible SDK) with function calling: create_note, update_note, delete_note, search_notes. The technique that made it reliable was context injection โ€” the full note list (IDs included) is injected into the system prompt ahead of the user's sentence, so a single instruction like "plan my day: workout, reading, and a grocery run" fans out into $k$ parallel, correctly-addressed tool calls in one model round-trip.
  • Database โ€” Neon Postgres, using two doors: a pooled connection string for runtime queries and a direct one (directUrl) for migrations.
  • Deploy โ€” Vercel for the frontend, Render for the API, environment-driven CORS, and committed lockfiles so every build installs the exact tested tree.

A piece of math I'm quietly proud of: access tokens expire every 15 minutes but are silently rotated from a 7-day refresh cookie โ€” a full week of continuous use rotates the token $\lceil 7 \times 24 \times 60 / 15 \rceil = 672$ times without ever showing a login prompt.

๐Ÿ“š What I learned

  • The browser is a deployment target too. VITE_* variables are baked into the bundle at build time โ€” editing them on the dashboard does nothing until you redeploy. This one idea explained two different "bugs" in production.
  • Databases have two doors. Neon's pooled endpoint is for queries; its direct endpoint is for migrations. Running prisma migrate deploy through a pooler fails in the most confusing ways imaginable.
  • Cookie semantics are origin-sensitive. SameSite=Strict and SameSite=None; Secure are not interchangeable settings โ€” they decide whether your auth works at all across domains.
  • Lockfiles are contracts. Committing package-lock.json turned "works on my machine" into "works on the machine that matters โ€” production."

๐Ÿงฑ Challenges I ran into

  1. The invisible cookie bug. Login succeeded, but sessions never persisted. Root cause: SameSite=Strict cookies are silently dropped on cross-origin requests (Vercel โ†” Render are different sites). The fix was SameSite=None; Secure in production only โ€” one line, two days of confusion.
  2. The build that failed far away. My first Render deploy died with TS7016 on every import. npm skips devDependencies when NODE_ENV=production โ€” and that's exactly where all my @types/* packages lived. Fix: npm ci --include=dev, plus committed lockfiles so builds stop re-rolling the dice on versions.
  3. The env variable that lied. The deployed app called /auth/login instead of /api/v1/auth/login because the API base URL env var was missing a path suffix โ€” and no amount of dashboard-editing fixed it until I redeployed, per lesson #1 above.
  4. Migrations through a pooler broke against Neon until I split url (pooled, for traffic) from directUrl (direct, for DDL). Two doors, two purposes.
  5. Voice typing in embedded browsers. Chromium webviews expose the SpeechRecognition API but lack Google's speech-service credentials, so dictation always fails there with a network error. I kept the feature with graceful degradation โ€” a clear fallback message instead of a broken button.

Try it: open imprint.iampranesh.me, sign up, and type "plan my day: workout, reading, and a grocery run" into the command bar. Then edit a note with the pencil, tick it complete, and ask the agent to clean up โ€” the grid reassembles itself while you watch.

Built With

Share this project:

Updates

Submission history