Connected Suite · System Design

Stewardship

Three apps, one typed data contract, built so the hard parts of life manage themselves.

A shared Bearer-token contract lets three independent Next.js apps pass job, housing, and financial data across each other server-side, so your life stays in sync without you re-entering anything.

Role Solo build · Product + Engineering
Stack Next.js · Supabase · Firebase · Plaid · Vercel · Claude
Contract version 1.0 · 2026-06-23

Life's hardest transitions are treated as separate problems

Landing a job, moving to a new city, getting your finances in order: these don't happen in isolation, but every tool for them assumes they do. So you carry the coordination by hand. You copy your salary into a budgeting sheet. You look up what rent in a new city costs against that number. You manually flag which apartments are realistic given the offer. The tools stay separate. You do the glue work.

The principle I built toward: done doesn't require you. A job signed in one app should ripple into the others automatically, with nothing in between except the contract.

Contract-first, producer-consumer, one direction

I wrote the data contract before building any UI. Each app was spec'd as a producer, consumer, or both against that contract. The integration layer is HTTP, not a shared database — which means each app is fully independent and could be replaced without touching the others.

Architecture diagram: Godspeed jobs feed into Provision and directly into Prosper; Provision listings feed into Prosper. One shared secret, server-side only, read-only pulls.
One direction only. No sync engine, no shared database, no circular dependencies. The shortcut arrow (Godspeed straight to Prosper) lets salary projections appear in Prosper without going through Provision first.

One shared secret, validated server-side on every request

All three apps share a single CROSS_APP_SECRET environment variable. Every cross-app fetch puts it in the Authorization header as a Bearer token. Every producer validates it before returning any data. The check happens inside the Next.js route handler — the browser never sees the secret and it never touches the client bundle.

Godspeed · app/api/external/jobs/route.ts
export async function GET(req: Request) {
  const auth = req.headers.get('authorization');
  const secret = process.env.CROSS_APP_SECRET;
  if (!secret || auth !== `Bearer ${secret}`) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }
  // ... return jobs
}
Prosper · src/lib/external.ts — both fetches use the same helper
async function getJSON<T>(url: string | undefined, key: string): Promise<T[]> {
  const secret = process.env.CROSS_APP_SECRET;
  if (!url || !secret) return [];
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${secret}` },
    next: { revalidate: 300 },   // 5-min server-side cache
  });
  if (!res.ok) return [];
  const data = await res.json();
  return Array.isArray(data?.[key]) ? (data[key] as T[]) : [];
}

The next: { revalidate: 300 } call means Prosper caches the cross-app responses for 5 minutes at the server layer — fast enough to feel instant, not so aggressive that a new job or listing is stale for long. Graceful degradation is also explicit: any network failure or bad status returns an empty array, so the Planning page degrades cleanly rather than throwing.

Typed interfaces, prefixed IDs, versioned and canonical

The contract lives as a CONTRACT.md in each repo — identical copies, intentionally. The canonical source is in Godspeed. Each object type uses a namespace prefix on its ID (gs_ for jobs, pv_ for listings) so Prosper can tell provenance at a glance with no ambiguity.

Godspeed produces

Job

Consumed by Provision (affordability) and Prosper (income projection)

interface Job {
  id: string;           // "gs_123" — gs_ prefix
  company: string;
  title: string;
  city: string | null;
  state: string | null;
  remote: boolean;
  salary_min: number | null;
  salary_max: number | null;
  salary_estimated?: boolean; // true when Godspeed
                              // estimates salary vs. listed
  status: "new" | "applied"
        | "interviewing"
        | "offer" | "rejected";
  source: string | null;
  url: string | null;
  updated_at: string;   // ISO 8601
}
Provision produces

Listing

Consumed by Prosper (upcoming housing costs and cash-flow projection)

interface Listing {
  id: string;               // "pv_456" — pv_ prefix
  name: string;
  city: string;
  state: string;
  rent: number;
  deposit: number | null;
  app_fee: number | null;
  beds: number | null;
  baths: number | null;
  sqft: number | null;
  lease_term_months: number | null;
  concessions: string | null;
  status: "saved" | "toured"
        | "applied" | "approved"
        | "signed" | "rejected";
  updated_at: string;       // ISO 8601
}

The 30% rule, automated

Provision's listings page fetches Godspeed jobs on load, then calls pickPrimaryJob() to select the most relevant one — offer status first, then interviewing, then most recently updated. That job's salary_min drives the affordability classification against each listing's rent.

Provision · src/lib/external/jobs.ts
export function classifyAffordability(
  rent: number | null,
  salaryMin: number | null,
): "affordable" | "stretch" | null {
  if (!rent || !salaryMin) return null;
  const monthlyGross = salaryMin / 12;
  return rent <= monthlyGross * 0.3 ? "affordable" : "stretch";
}

The result is a badge on every listing card — no manual calculation required. When no Godspeed data is available (user hasn't connected or Godspeed is unreachable), the function returns null and the badge is omitted rather than showing a wrong value.

Three apps, three roles, two scheduled jobs

Producer

Godspeed

Job-Search Dashboard · Next.js + Supabase

  • Clearance — AI cover letter generator tailored to each role from your profile
  • Resume builder — experience bank plus targeted resume export to PDF
  • People & outreach — Hunter.io surfaces contacts at target companies
  • Skills board — daily aptitude and product interview practice
  • Live job + news cron — postings refresh at 1pm UTC, company news at 9pm UTC

The longer you use it, the richer your experience bank gets — making every future resume and cover letter faster and more targeted than the last.

Full case study →
Reader + Producer

Provision

Moving Command Center · Next.js + Firebase

  • Listings pipeline — tracks each apartment from saved through signed with rent, deposit, and lease terms
  • AI checklists — phased moving checklist generated and tailored to your specific move
  • Roommate scorer — weighted criteria plus an AI fit assessment per candidate
  • Affordability badge — reads Godspeed salary and flags every listing as affordable or a stretch via the 30% rule

A move normally lives across browser tabs and a dying spreadsheet. Provision makes it one organized place you can hand off to your future self the next time you move.

Full case study →
Consumer

Prosper

Personal Finance Dashboard · Next.js + Supabase + Plaid

  • Plaid sync — real bank, card, and loan balances refreshed daily at 9am UTC with no action required
  • Sparks — AI money tutor that answers using your actual balances, not generic advice
  • Debt payoff planner — avalanche vs. snowball side by side with an exact debt-free date
  • Planning view — runs salary from Godspeed and rent from Provision against your real accounts in one projection

Your full financial picture updates automatically every day. Over time Sparks builds a learning history tied to your real numbers, so advice gets more relevant the longer you use it.

Full case study →

What I chose and why

Each app has its own database. Integration happens at the API layer, not the data layer. This means each app is independently deployable and the integration contract is explicit and versioned rather than implicit table access. The tradeoff is latency on cross-app reads — which is why Prosper adds a 5-minute server cache.

Consumers only read. No app can write to another app's data. This prevents the class of bugs where a downstream app corrupts an upstream source of truth. The cost is that there's no real-time push — consumers pull when they need data, not when producers update.

Every cross-app fetch returns an empty array on any error — network failure, wrong status, missing env var. Pages that consume cross-app data are built to render without it. A recruiter using Prosper's Planning page never sees a broken state just because Godspeed is unreachable.

One type mismatch between consumers

Prosper's Job interface includes salary_estimated?: boolean as an optional field. Provision's ContractJob interface doesn't define it — so Provision silently ignores the field even though Godspeed sends it. This doesn't break anything (Provision doesn't use that field), but it's a contract drift that a stricter shared-types package would prevent. In a team environment I'd extract the interfaces into a published internal package both consumers import from, so a producer changing its schema breaks the consumer's build before it breaks production.

I don't build screens. I build systems that talk to each other.

The three apps are in real use. Two Vercel cron jobs keep Godspeed's data fresh on a schedule. A third keeps Prosper's Plaid balances current. Cross-app data moves at request time with a 5-minute cache. Manual re-entry is gone — a job change in Godspeed changes what's affordable in Provision and what the income projection shows in Prosper, automatically.

2 Typed contract endpoints
/api/external/jobs · /api/external/listings
3 Vercel cron jobs across the suite
jobs 1pm UTC · news 9pm UTC · Plaid 9am UTC
0 Shared databases
Integration is at the API layer, not the data layer