๐ 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
httpOnlycookies, 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 deploythrough a pooler fails in the most confusing ways imaginable. - Cookie semantics are origin-sensitive.
SameSite=StrictandSameSite=None; Secureare not interchangeable settings โ they decide whether your auth works at all across domains. - Lockfiles are contracts. Committing
package-lock.jsonturned "works on my machine" into "works on the machine that matters โ production."
๐งฑ Challenges I ran into
- The invisible cookie bug. Login succeeded, but sessions never persisted. Root cause:
SameSite=Strictcookies are silently dropped on cross-origin requests (Vercel โ Render are different sites). The fix wasSameSite=None; Securein production only โ one line, two days of confusion. - The build that failed far away. My first Render deploy died with
TS7016on every import. npm skips devDependencies whenNODE_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. - The env variable that lied. The deployed app called
/auth/logininstead of/api/v1/auth/loginbecause 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. - Migrations through a pooler broke against Neon until I split
url(pooled, for traffic) fromdirectUrl(direct, for DDL). Two doors, two purposes. - Voice typing in embedded browsers. Chromium webviews expose the
SpeechRecognitionAPI but lack Google's speech-service credentials, so dictation always fails there with anetworkerror. 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
- express.js
- gemini-api
- neondb
- node.js
- openai-sdk
- postgresql
- prisma
- react
- tool-calling
- typescript
- zod