AI-Powered Professional Portfolio Manager

Local-first profile and resume builder

On-goingPersonalSolo full-stack engineer5 min read

I built a Next.js app where one profile holds the facts, and tailored resumes pick from that inventory. Export produces LaTeX matching my reference template, with an interim browser PDF path while the engine story settles.

Stack · Next.js · React · TypeScript · Bun · SQLite (better-sqlite3) · Zod · shadcn/ui · Tailwind CSS · jsPDF · Docker Compose · LaTeX export

Demo coming soonRepo coming soon

Images coming soon

The brief

Profile Manager is a local-first web app I am building for myself as a software professional. One profile is the place facts get written. Resumes select from that inventory, override a few fields when a job needs different framing, and export to LaTeX that follows my reference resume template.

I am the sole full-stack engineer: product shape, domain rules, UI, SQLite persistence, compile path, tests, and Docker Compose packaging. v1 does not use an LLM to invent claims. The app stores and compiles what I typed.

What I'm solving

I kept the same career facts in several places: a LaTeX source of truth, ad hoc edits per application, and no clean split between "what is true about me" and "what this resume shows." Tailoring usually meant copy-paste, then drift when the profile changed.

I also wanted compile-time honesty. If work experience or skills are thin, I want a flag on the resume workspace, not a silent pretty PDF. The pain is personal and operational: one author, many applications, and a hard requirement that printed claims stay grounded in authored data.

How it works

The home dashboard summarizes what I have already authored. /profile is a single scrollable workspace with Read and Edit modes. Identity, contacts, summary, and locations save together. Skills, experience, projects, education, certifications, and references use add-on-demand forms. Skills carry a locked type (language, tool, platform, or practice) and optional capabilities, such as Spring Boot under Java.

/resumes lists the Master resume plus job resumes with a label and optional description. Opening a resume is the compile workspace: choose which skills and tools appear, pick capabilities, run export and compile from the page header. Master stays tied to the full profile inventory; a job resume can start from Master or from scratch and only cite ids that exist on the profile.

Export writes .tex aimed at Overleaf / pdflatex parity with reference_resume.tex. Browser PDF via jsPDF is an interim path. Compile can also record snapshots and surface eval flags (for example missing work experience or a skill with no capabilities). An applications tracker route exists in the chrome; much of that surface is still gated while the profile and resume verticals deepen.

Architecture choices

Stack: Next.js and React with TypeScript for the App Router UI, Bun for day-to-day dev, SQLite through better-sqlite3 for the store, Zod for validation, shadcn/ui and Tailwind for the design system, Node plus tsx for tests (Bun's test runner and native SQLite did not cooperate here), and Docker Compose for a promotion build that binds localhost and keeps the database on a named volume.

I treat the profile as the only write surface for facts. Resumes hold which entries are included plus overrides, not a second full copy of every entry. That keeps Master honest when the profile grows, and it keeps job resumes from inventing ids the profile never had.

A few moments that shaped the build:

Scroll-all-sections on Profile instead of a step wizard. I wanted a full Read view of the person and one Save for basics, with list sections owning their own add/save actions. Soft step-gating helpers exist in tests but stay unwired until I decide they help more than they interrupt.

Skill capabilities as free-text detail under a skill, authored on the profile first, then optionally picked per resume. Compile prints one table row per included skill or tool, with chosen capabilities in the value column. That matches how I talk about a stack without turning capabilities into a second taxonomy.

LaTeX export locked to the reference preamble and structure so Overleaf stays the fidelity check. Visual spacing still lags the paper reference in places; I shipped compile correctness before chasing pixel parity. When pdflatex is available in the environment, tests can exercise a real engine path; otherwise that case skips.

Skeletal chrome with gated unfinished regions. Routes for dashboard, profile, resumes, applications, and legal exist early so the product shape is visible while I ship vertical slices rather than finishing one screen in isolation.

What shipped

Working surfaces today: dashboard stub over authored data, profile workspace with Read/Edit and global basics Save, resume gallery with Master and tailored cards, resume workspace with include chips, capability picks, eval flags, .tex export aligned to the reference contract, and interim PDF via jsPDF. Persistence is local SQLite; Compose runs a production-style container with the database on a volume.

Node tests cover compile behavior (skill order on the resume, capability rendering, eval flags, TeX escaping and preamble checks). As of early September 2026 the suite was in the low nineties of passing tests with the pdflatex case skipped when the engine is missing.

Still open: resume overrides for basics (headline, summary, location subset), layout fidelity against the paper reference, engine-backed PDF as a first-class path, and fuller applications tracking. I have been holding commits until an MVP pass on local Docker Compose.

TODO: public live URL and GitHub remote when I publish the repo

What I'd do again

Separate "facts I authored" from "what this resume includes" early. Mixing them makes every tailor a fork you never reconcile.

Ship a real export contract against a file you already trust (here, the reference .tex) before polishing print spacing. Correct structure is easier to verify than "looks fine in the browser."

Keep native modules and the test runner honest. Choosing Node for tests while Bun drives the app was awkward, but it beat fighting a fatal native addon path.

Leave unfinished chrome gated. Empty routes that look clickable waste trust; a skeleton with clear gates kept the map visible without pretending the tracker was done.