# Project export: Change

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: Make a change with your change.
- Devpost: https://devpost.com/software/change-m932ik
- GitHub: https://github.com/dzymachine/Change.git
- Demo: https://empowered-onboarding-054129.framer.app/
- Video: https://www.youtube.com/embed/RYrYNLnU0cI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Sponsor - Framer] Best use of Framer)
- Team: 4 GitHub contributor(s) — Dessy Bonev (38 commits), pshapoorian (17 commits), Isaiah (16 commits), Isaiah (1 commits)

## Devpost submission (written by the team)

### Overview

Who We Are Change is a non-profit donation platform that streamlines everyday transactions into actionable donations by rounding up purchases to the nearest dollar.

### Inspiration

Our idea for Change came from our own desire to give back to local communities in a way that felt manageable and realistic. We were largely inspired by Acorns: the incremental investing app that rounds up your purchases and invests the difference for you. Change works very similarly, but instead funnels the accumulated amount from rounded up payments into charities and non-profits that users feel passionate about while also helping users track, organize, and prepare their donations with clear, itemized tax-ready summaries. What It Does Change tracks a user’s transactions on selected bank accounts via Plaid and rounds up any purchases they make to the nearest dollar. Then, Change donates the amount to a charity/nonprofit according to the user’s selection and configuration. Users can select up to 5 charities/nonprofits to donate to at a time and then set goals they’d like to reach for each one. They can also choose to randomly disperse their change amongst all of their selected charities or complete goals one at a time (priority queue). As these micro-donations accumulate, Change automatically organizes them into a centralized donation history and tax-ready summary, giving users a clear, itemized, year-at-a-glance view of their contributions that’s easy to review, export, and reference during tax season. How We Built It We built our landing page on Framer and deployed the app on Vercel. We used Next.js to develop our user interface, React as our server router, TypeScript for all of our application code, and Tailwind CSS for styling. Supabase stores our user authentication and user data. In order to create our donation options, we used GlobalGiving.org’s API to pull charity and non-profit information from their corresponding sites in real time. We also integrated with Plaid to allow our users to seamlessly connect their bank accounts so that the server can track account transactions. We do this using Plaid webhooks to trigger server-side polling. Donation and transaction data is processed through a dedicated aggregation layer to power Change’s Tax Center, which generates annual summaries, per-charity breakdowns, and exportable tax-ready reports (PDF and CSV). Nodemailer is also used to send users emails when they achieve their donation goals and Git managed our version control throughout the development of the project. Challenges We Ran Into While we hit obstacles while deploying our app into production with Vercel and getting our database to work, our major struggles all surrounded Plaid. Testing with the Plaid Sandbox was strenuous because we couldn’t get new transactions in Plaid to properly sync and trigger the webhook. This resulted in many hours of transaction testing where we had to send tons of manual curl commands in Sandbox mode before we could confidently try moving into Production. We always had the goal of moving from Sandbox to Production with Plaid and had been on track to do so before realizing that we needed to get manual approval from Plaid (which took 1-2 business days) in order to use their production keys. This was a disappointing stage in our development as we had obsessively prepared the sandbox environment to smoothly transition over into production, and doing so meant that we couldn’t show off how our product works with real purchases. We also experienced many difficulties rendering images for charities and nonprofits via Global Giving’s API, which proved to be very limited without doing their lengthy KYC registration. We solved this by manually inputting the charity image links, as relying on the ones pulled from the API resulted in poor resolution. Another roadblock we faced was in charging users real money via Stripe, as doing so would require us to register with their KYC protocol as well. Instead, we opted to focus on using Plaid to demonstrate how our system gets triggered by real purchases-- with the roundup charge and donation happening in sandbox. Accomplishments That We're Proud Of Our biggest accomplishment with Change is executing a production-ready purchase to donation workflow that triggers on transactions that users make. Learning how to use Plaid and get real transactions to trigger our server’s workers was a stretch goal that our team worked hard to achieve. We are also really proud of our UI that was put together using Framer and Next.js. We wanted our users to undergo a simple, yet visually pleasing onboarding experience that would make them excited about donating, both on the web and on mobile. Lastly, we are very pleased to have been able to successfully deploy our work and get to share it with friends and family who are interested in using Change. What We Learned Seeing as this is all of our first hackathon, we had to learn a lot. We learned how payment transactions operate on a granular level and the legal constraints involved in charging users, tracking their purchases, and holding money in escrow. We all also learned how to deploy a server (first time for all of us!) using Vercel and write database migrations in Supabase that supported the new features we came up with along the way. We also learned how to use Framer to create the beautiful landing page and route that to our main site. Unexpectedly, we also ended up learning a lot about donation based systems and the non-profits/charities that impact our local community. It was empowering to see how many people dedicate their lives to social justice and bettering the world they live in. The importance of supporting these communities became ever-so present as we worked on this project and gave us a new level of respect for those who make these organizations possible.

### What's next

Obviously, we would love to use Plaid in production and see our app work with real payments (once they approve of our production access request on Monday). However, the ideation process with Change never ended, and our vision for this product expands farther than the horizon of CruzHacks. We would like to streamline real donation payments via Global Giving by creating real charges via Stripe and shift the entire product into production. Eventually, it would be great to grow a community on Change and add a community feature that helps users connect to one another and share charities that they care about.

## README (from the GitHub repository)

# Change.

Small donations that add up. Change connects a user's bank account, rounds up everyday purchases, and allocates the spare change toward charitable goals the user sets.

## Product Overview

### Problem
Most people want to give back, but regular donations are easy to forget and hard to budget. Small, automatic contributions make it effortless to support causes consistently.

### Solution
Change turns everyday spending into impact:
- Link a bank account via Plaid.
- Pick charities and set goals.
- Automatically round up eligible purchases and allocate the change.
- Celebrate when a goal is completed.

### Key Features
- Onboarding flow for charity selection and goal setting.
- Donation modes: priority (fill goals in order) or random (spread impact).
- Charity discovery with filters and curated lists.
- Transaction roundup tracking and donation batching.
- Goal completion emails.

## Architecture

### System Components
- **Frontend**: Next.js App Router UI for onboarding, dashboard, and settings.
- **Backend**: Next.js Route Handlers for API endpoints and webhooks.
- **Database**: Supabase Postgres for users, charities, goals, transactions, and donations.
- **Integrations**:
  - Plaid for bank account linking and transaction syncing.
  - GlobalGiving for featured charity data.
  - SMTP (Nodemailer) for email notifications.

### Data Flow
1) User links their bank account via Plaid Link.
2) Plaid transactions are synced and stored in Supabase.
3) Each transaction is rounded up and allocated based on the user's donation mode.
4) When a charity goal is completed, an email is sent.
5) Donations can be batched once thresholds are met.

### Core Modules
- `lib/plaid/*`: Plaid client and sync utilities.
- `lib/donations/*`: Round-up calculation, allocation, and batching.
- `lib/charities/data.ts`: Curated and local charities.
- `lib/email/sendDonationEmail.ts`: Goal completion emails.

### API Endpoints (selected)
- `app/api/plaid/create-link-token/route.ts`: Plaid Link init.
- `app/api/plaid/exchange-token/route.ts`: Exchange public token.
- `app/api/plaid/webhook/route.ts`: Plaid webhooks.
- `app/api/charities/route.ts`: Charity feed (curated + GlobalGiving).

## Tech Stack
- Next.js 16 (App Router)
- React 19
- Supabase (Auth + Postgres)
- Plaid
- GlobalGiving API
- Tailwind CSS
- Nodemailer

## Local Development

```bash
npm install
npm run dev
```

### Environment Variables
Set these in `.env`:
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- `SUPABASE_SERVICE_ROLE_KEY`
- `PLAID_CLIENT_ID`
- `PLAID_SECRET`
- `PLAID_ENV`
- `PLAID_WEBHOOK_URL`
- `GLOBALGIVING_API_KEY`
- `SMTP_HOST`
- `SMTP_PORT`
- `SMTP_USER`
- `SMTP_PASS`
- `SMTP_FROM`
- `SMTP_SECURE`

## Notes for Devpost
- This repo contains both product UX and the full transaction-to-donation pipeline.


## Detected evidence (automated analysis)

Indexed codebase: 110 recognized source files, 461 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (118 of 118)

```
.gitignore
actions/charities.ts
actions/donations.ts
actions/plaid.ts
actions/user.ts
app/(dashboard)/dashboard/page.tsx
app/(dashboard)/layout.tsx
app/(dashboard)/profile/page.tsx
app/(dashboard)/settings/page.tsx
app/(dashboard)/tax-center/page.tsx
app/api/charities/route.ts
app/api/debug/accounts/route.ts
app/api/debug/check-item/route.ts
app/api/debug/fire-webhook/route.ts
app/api/debug/manual-sync/route.ts
app/api/debug/refresh-transactions/route.ts
app/api/debug/reset-cursor/route.ts
app/api/debug/reset-user-data/route.ts
app/api/debug/sandbox-create-transactions/route.ts
app/api/debug/simulate-transaction/route.ts
app/api/debug/transactions/route.ts
app/api/debug/update-webhook-url/route.ts
app/api/debug/user-charities/route.ts
app/api/debug/user-data/route.ts
app/api/demo/simulate-purchase/route.ts
app/api/globalgiving/featured/route.ts
app/api/health/env/route.ts
app/api/health/route.ts
app/api/plaid/create-link-token/route.ts
app/api/plaid/exchange-token/route.ts
app/api/plaid/webhook/route.ts
app/api/tax-summary/export/csv/route.ts
app/api/tax-summary/export/pdf/route.ts
app/api/tax-summary/route.ts
app/api/transactions/route.ts
app/auth/callback/route.ts
app/globals.css
app/layout.tsx
app/onboarding/charities/page.tsx
app/onboarding/donation-mode/page.tsx
app/onboarding/goals/page.tsx
app/onboarding/layout.tsx
app/onboarding/plaid/page.tsx
app/page.tsx
components/auth/AuthLanding.tsx
components/auth/LogoutButton.tsx
components/auth/SignOutButton.tsx
components/dashboard/AddCharityModal.tsx
components/dashboard/CharitiesSection.tsx
components/dashboard/CharityCard.tsx
components/dashboard/CharityGoals.tsx
components/dashboard/DashboardNav.tsx
components/dashboard/DonationChart.tsx
components/dashboard/ImpactCard.tsx
components/dashboard/TopNav.tsx
components/dashboard/TransactionList.tsx
components/demo/SimulatePurchase.tsx
components/onboarding/CharityPicker.tsx
components/plaid/LinkBankButton.tsx
components/settings/ResetDemoDataButton.tsx
components/settings/RoundupToggle.tsx
components/tax-center/CharityBreakdown.tsx
components/tax-center/ExportButtons.tsx
components/tax-center/GivingInsights.tsx
components/tax-center/MonthlyChart.tsx
components/tax-center/TaxDisclaimer.tsx
components/tax-center/TaxSummaryCard.tsx
components/tax-center/YearSelector.tsx
eslint.config.mjs
hooks/use-plaid-link.ts
hooks/use-user.ts
lib/app-url.ts
lib/charities/data.ts
lib/debug-auth.ts
lib/donations/allocate.ts
lib/donations/calculate.ts
lib/donations/process.ts
lib/email/index.ts
lib/email/sendDonationEmail.ts
lib/log-json.ts
lib/mock-data.ts
lib/plaid/client.ts
lib/plaid/sync.ts
lib/plaid/verify-webhook.ts
lib/supabase/admin.ts
lib/supabase/client.ts
lib/supabase/server.ts
lib/utils/currency.ts
next-env.d.ts
next.config.ts
package.json
postcss.config.mjs
proxy.ts
README.md
supabase/.gitignore
supabase/config.toml
supabase/migrations/20260117065853_init_schema.sql
supabase/migrations/20260117070112_002_profiles.sql
supabase/migrations/20260117070118_003_user_charities.sql
supabase/migrations/20260117070128_004_donations.sql
supabase/migrations/20260117070132_005_indexes.sql
supabase/migrations/20260117070804_006_add_fks_charities_transactions.sql
supabase/migrations/20260117071756_007_rls_policies.sql
supabase/migrations/20260117072758_008_profiles_trigger.sql
supabase/migrations/20260117080000_009_add_selected_charity.sql
supabase/migrations/20260117100000_010_linked_accounts.sql
supabase/migrations/20260117100001_011_transactions.sql
supabase/migrations/20260117110000_012_charity_id_to_text.sql
supabase/migrations/20260118030000_013_fix_donated_to_charity_id_type.sql
supabase/migrations/20260118040000_014_plaid_event_logs.sql
supabase/migrations/20260118050000_015_add_charity_category.sql
supabase/quick-fix.sql
supabase/schema.sql
tsconfig.json
types/database.ts
types/index.ts
types/plaid.ts
vercel.json
```

### Dependencies

- package.json: @supabase/ssr@^0.8.0, @supabase/storage-js@^2.90.1, @supabase/supabase-js@^2.90.1, @tailwindcss/postcss@^4, @types/node@^20, @types/nodemailer@^7.0.5, @types/react@^19, @types/react-dom@^19, @vercel/functions@^3.3.6, eslint@^9, eslint-config-next@16.1.2, jose@^6.1.3, next@16.1.2, nodemailer@^7.0.12, plaid@^41.0.0, react@19.2.3, react-dom@19.2.3, react-plaid-link@^4.1.1, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- Revert plaid onboarding page to previous design
- Add Tax Center for donation summaries and exports
- Mobile dashboard + onboarding polish
- Redirect sign out to Framer landing page
- Remove Malaria Consortium charity (broken image link)
- Remove decorative circle from login page
- Add round-up toggle persistence and demo data reset
- Fix TypeScript error: make allocation.charityId optional
- Show charity category instead of hardcoded "Santa Cruz" location
- Fix transactions not showing in production profile page
- Replace text logo with PNG brand logo across all pages
- Consolidate simulate purchase buttons into single button in CharitiesSection
- Add rounded corners to time range and simulate payment buttons
- Soften dashboard UI with rounded corners and hover effects
- hoepfully vercel deployed
- fixed resolution
- Add demo mode for hackathon judges
- Redesign login page with new brand color palette and typography
- edits of readmore on dashboard and removing setting up acc
- Add production readiness check to health endpoint

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "change-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@supabase/ssr": "^0.8.0",
    "@supabase/storage-js": "^2.90.1",
    "@supabase/supabase-js": "^2.90.1",
    "@vercel/functions": "^3.3.6",
    "jose": "^6.1.3",
    "next": "16.1.2",
    "nodemailer": "^7.0.12",
    "plaid": "^41.0.0",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "react-plaid-link": "^4.1.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/nodemailer": "^7.0.5",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.2",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Roboto_Serif, Playfair_Display, JetBrains_Mono } from "next/font/google";
import "./globals.css";

const robotoSerif = Roboto_Serif({
  variable: "--font-body",
  subsets: ["latin"],
  weight: ["300", "400", "500", "600"],
});

const playfairDisplay = Playfair_Display({
  variable: "--font-display",
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
});

const jetbrainsMono = JetBrains_Mono({
  variable: "--font-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Change - Round Up Donations",
  description: "Turn your spare change into real change. Round up every purchase and donate to charity.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${robotoSerif.variable} ${playfairDisplay.variable} ${jetbrainsMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import { AuthLanding } from "@/components/auth/AuthLanding";

// Note: Middleware handles all auth redirects:
// - Authenticated users with completed onboarding → /dashboard
// - Authenticated users without completed onboarding → /onboarding/charities
// - Unauthenticated users → stay here (login page)

// Force dynamic rendering - this page uses Supabase auth which requires runtime env vars
export const dynamic = "force-dynamic";

export default function HomePage() {
  return (
    <main 
      className="min-h-screen flex items-center justify-center relative overflow-hidden"
      style={{ backgroundColor: "var(--white)" }}
    >
      {/* Subtle background decoration - asymmetric for organic feel */}
      <div 
        className="absolute top-0 right-0 w-1/3 h-1/2 opacity-[0.03]"
        style={{ 
          backgroundColor: "var(--tan)",
          transform: "skewY(-12deg) translateY(-20%)",
        }}
      />
      <div 
        className="absolute bottom-0 left-0 w-1/4 h-1/3 opacity-[0.02]"
        style={{ 
          backgroundColor: "var(--green)",
          transform: "skewY(6deg) translateY(30%)",
        }}
      />
      
      {/* Main content */}
      <div className="relative z-10 w-full py-12 md:py-20">
        <AuthLanding />
      </div>
    </main>
  );
}

```

### types/index.ts

```typescript
/**
 * Shared application types
 */

// User profile (extends Supabase auth.users)
export interface Profile {
  id: string;
  email: string | null;
  displayName: string | null;
  onboardingCompleted: boolean;
  selectedCharityId: string | null;
  roundupEnabled: boolean;
  createdAt: string;
}

// Linked bank account (Plaid item)
export interface LinkedAccount {
  id: string;
  userId: string;
  plaidItemId: string;
  institutionName: string | null;
  institutionId: string | null;
  isActive: boolean;
  createdAt: string;
}

// Transaction from Plaid
export interface Transaction {
  id: string;
  userId: string;
  linkedAccountId: string;
  plaidTransactionId: string;
  amount: number;
  roundupAmount: number | null;
  merchantName: string | null;
  category: string[] | null;
  date: string;
  isDonation: boolean;
  isPending: boolean;
  processedForDonation: boolean;
  createdAt: string;
}

// Charity for donations
export interface Charity {
  id: string;
  name: string;
  description: string | null;
  logoUrl: string | null;
  websiteUrl: string | null;
  isActive: boolean;
}

// Donation batch
export interface Donation {
  id: string;
  userId: string;
  charityId: string;
  amount: number;
  transactionCount: number;
  status: DonationStatus;
  processedAt: string | null;
  createdAt: string;
}

export type DonationStatus = "pending" | "processing" | "completed" | "failed";

// Onboarding state
export interface OnboardingState {
  step: 1 | 2 | 3;
  bankLinked: boolean;
  charitySelected: boolean;
}

// Dashboard stats
export interface DashboardStats {
  totalDonated: number;
  transactionsCount: number;
  currentCharity: string | null;
  monthlyAverage: number;
  pendingRoundup: number;
}

```

### lib/email/index.ts

```typescript
// Barrel export for email module
export { sendDonationEmail } from "./sendDonationEmail";

```

### lib/supabase/server.ts

```typescript
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";

export async function createClient() {
  const cookieStore = await cookies();
  type CookieOptions = Parameters<typeof cookieStore.set>[2];
  type CookieToSet = { name: string; value: string; options?: CookieOptions };

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet: CookieToSet[]) {
          // Next.js server components can't set cookies directly unless in a Route Handler / Middleware.
          // This is okay here; session refresh is usually handled in middleware.
          try {
            cookiesToSet.forEach(({ name, value, options }) => {
              cookieStore.set(name, value, options);
            });
          } catch {
            // Ignore if called from a Server Component
          }
        },
      },
    }
  );
}

```

### app/(dashboard)/layout.tsx

```typescript
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import { TopNav } from "@/components/dashboard/TopNav";

// Note: Middleware handles onboarding redirects, but we keep auth check here for defense in depth

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    redirect("/");
  }

  // Double-check onboarding status (middleware should handle this, but just in case)
  const { data: profile } = await supabase
    .from("profiles")
    .select("onboarding_completed")
    .eq("id", user.id)
    .single();

  if (!profile?.onboarding_completed) {
    redirect("/onboarding/charities");
  }

  return (
    <div 
      className="min-h-screen"
      style={{ backgroundColor: "var(--background)" }}
    >
      <TopNav />
      <main className="max-w-6xl mx-auto w-full px-4 sm:px-6 py-6 sm:py-10">{children}</main>
    </div>
  );
}

```

### app/onboarding/layout.tsx

```typescript
import { redirect } from "next/navigation";
import Image from "next/image";
import { createClient } from "@/lib/supabase/server";
import { SignOutButton } from "@/components/auth/SignOutButton";

// Note: Middleware handles redirects, but we keep checks here for defense in depth

export default async function OnboardingLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    redirect("/");
  }

  // Check if user has already completed onboarding
  const { data: profile } = await supabase
    .from("profiles")
    .select("onboarding_completed")
    .eq("id", user.id)
    .single();

  if (profile?.onboarding_completed) {
    redirect("/dashboard");
  }

  return (
    <div 
      className="min-h-screen flex flex-col"
      style={{ backgroundColor: "var(--white)" }}
    >
      {/* Header */}
      <header 
        className="py-5 px-6"
        style={{ borderBottom: "1px solid var(--border)" }}
      >
        <div className="max-w-4xl mx-auto flex items-center justify-between">
          <Image
            src="/change-logo.png"
            alt="Change"
            width={100}
            height={27}
            priority
          />
          <SignOutButton variant="compact" />
        </div>
      </header>

      {/* Main content */}
      <main className="flex-1 flex items-start justify-center px-6 py-12">
        <div className="w-full max-w-3xl">{children}</div>
      </main>
    </div>
  );
}

```

### app/api/health/route.ts

```typescript
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({
    status: "ok",
    timestamp: new Date().toISOString(),
    app: "Change",
    message: "If you see this, ngrok is working!",
  });
}

```

### app/auth/callback/route.ts

```typescript
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");
  const next = requestUrl.searchParams.get("next") ?? "/";

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    
    if (!error) {
      // Redirect to the intended destination after successful auth
      return NextResponse.redirect(new URL(next, requestUrl.origin));
    }
  }

  // If there's an error or no code, redirect to home with error
  return NextResponse.redirect(new URL("/?error=auth_callback_error", requestUrl.origin));
}

```

[100 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]