Skip to content

Latest commit

 

History

848 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EECS Capstone App

The Oregon State University EECS Capstone application: browse and propose capstone projects, run them through a review workflow, and manage shared inventory.

This README covers how to run and develop the app. Known issues and the roadmap live in GitHub Issues. For the full, exhaustive feature list, see PRD.md. For implementation quirks and gotchas, see docs/QUIRKS.md. For how work moves through the repo and the gates it passes, see CONTRIBUTING.md; for the rules that bind an agent on every turn, AGENTS.md.

Known issues and roadmap

Both live in GitHub Issues rather than in this file, so that what is outstanding has one home that can be assigned, closed and linked from a PR.

For the exhaustive list of what is already built, see PRD.md.

Getting Started

New to the repo? docs/ONBOARDING.md is day one: the accounts to ask for, a wizard that runs the steps below and every test suite once (bash scripts/onboard.sh), what Claude Code does here, and a first week.

npm install
docker compose up -d
npm run db:migrate    # nothing applies migrations on boot
npm run dev

To stop the database and storage:

docker compose down

Port conflicts

The stack publishes Postgres on 5432 and RustFS on 9000/9001. If another project already holds those host ports, the containers still start but silently publish nothing, and the app talks to the other project's services instead: Postgres fails with password authentication failed, while S3 may quietly accept writes, because the default rustfsadmin credentials are the same.

Give this stack its own host ports in .env (docker compose reads .env, and not .env.local), then mirror them in .env.local:

# .env, read by docker compose
POSTGRES_PORT=5433
STORAGE_PORT=9100
STORAGE_CONSOLE_PORT=9101
# .env.local, read by the app and the scripts
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/eecs_capstone"
S3_ENDPOINT=http://localhost:9100
VITE_STORAGE_PUBLIC_BASE=http://localhost:9100/cs-capstone

Then docker compose up -d --force-recreate. Confirm the ports actually bound with docker compose ps and check the publishers column is not empty.

To build for production:

npm run build

Architecture

This is a TanStack Start app (React SSR) with TanStack Router (file-based routes in src/routes), TanStack Query, Drizzle ORM on PostgreSQL, Better Auth, and S3-compatible object storage (RustFS locally). UI is shadcn/ui + Radix.

A few conventions worth knowing before you contribute:

  • The project workflow state machine (src/lib/project-workflow.ts) and visibility rules (src/lib/project-visibility.ts) are pure modules. Keep business logic there, not in routes.
  • A project's proposer is proposer_id when an account exists and proposer_email otherwise. Staff cannot retype a linked address directly; the edit form routes that through a re-assign modal (see docs/QUIRKS.md).
  • Every project/comment/inventory mutation is one server function in src/server/, each enforcing its own gate and wrapping writes in a transaction. The companion *As(viewer, ...) helpers next to each createServerFn let integration tests exercise business logic directly, without the HTTP layer.
  • Forms with more than two fields use TanStack Form with Zod validators shared with the server. Server-thrown ZodError is mapped back to field-level errors via src/lib/apply-server-errors.ts.
  • Full-text search uses a Postgres generated tsvector column with a GIN index. To change field weights, drop and re-add the column in a new migration (see docs/QUIRKS.md).
  • Interest-based recommendations use pgvector. Projects are embedded only on publish and re-embedded when a published project's indexed text changes; see src/server/_internal/project-embeddings.ts, the single writer of every vector. npm run embeddings:backfill is the safety net.
  • All filter/search state lives in URL search params so links are shareable.

Authentication setup (Better Auth)

This project uses Better Auth backed by Drizzle + Postgres. Identity lives in the user, session, account, and verification tables (generated by Better Auth's CLI into src/db/auth-schema.ts and re-exported from src/db/schema.ts).

  1. Copy .env.example to .env.local and fill in values.

  2. Generate a Better Auth secret if you don't have one:

    npx -y @better-auth/cli secret
  3. Register a GitHub OAuth App at https://github.com/settings/developers with callback http://localhost:3000/api/auth/callback/github, then put the credentials into .env.local.

  4. Start Postgres and run the dev server:

    docker compose up -d
    npm run dev
  5. Seed your dev database (safe to re-run):

    npm run db:seed:dev

    In production, seed an admin user (configured via environment variables).

Keep at least two admin users in production. The self-action guard prevents a sole admin from accidentally demoting or banning themselves into a one-way trap. Use npm run db:seed:admin or a direct db:studio edit to bootstrap the second admin.

Changing the Better Auth schema

src/db/auth-schema.ts is hand-maintained. Do not run @better-auth/cli generate against it: that package lags the library (the CLI is stuck on 1.4.x while we run better-auth 1.6.x), and its output silently drops the timezone-aware timestamps, the session/account/verification indexes, and the role NOT NULL default that this file carries. Running it would produce a destructive migration.

To add or change a Better Auth additionalField:

  1. Add the field to user.additionalFields in src/lib/auth.ts (this is what Better Auth reads at runtime).

  2. Add the matching column to the user table in src/db/auth-schema.ts by hand, with the correct type and a DB default (so existing rows backfill).

  3. Generate and apply the migration, reviewing the SQL to confirm it only adds your column:

    npm run db:generate
    npm run db:migrate

If the @better-auth/cli package ever catches up to the installed better-auth version, this file could return to CLI generation; until then, edit it directly.

Email transport

EMAIL_TRANSPORT selects the sender behind the EmailSender interface in src/lib/email/sender.ts:

  • console (the default, and what local development uses): every email below is written to the server's stderr instead of being sent, review notices included.
  • ses: real outbound mail through AWS SES v2 (src/lib/email/ses-sender.ts), which additionally requires EMAIL_FROM to be a verified sender identity. EMAIL_REPLY_TO is optional: set it and every message carries that Reply-To, leave it blank and the header is omitted.

The app sends four emails, all through src/lib/email/templates.ts:

Email Trigger Recipient
Verify your email Sign-up The new account
Reset your password Forgot-password form The account
New project submitted A project moves to submitted EMAIL_REVIEW_INBOX
Approved / Changes requested Staff review a project The proposer

Everything else the app notifies about is in-app only, a row in notifications rendered by the bell, and never reaches an inbox. Staff can skip either review email per action from the transition dialog.

Production runs ses and has done since task definition revision 22: the domain identity verifies with DKIM SUCCESS, the account has production access (so the sandbox recipient restriction no longer applies), and mail sends From noreply@capstone.eecs.oregonstate.edu.

EMAIL_FROM must align with the verified identity or DKIM fails, which is why both it and var.domain_name derive from the same variable in infra/ecs.tf. It is also not optional under ses: getEmailSender() runs at module scope in src/lib/auth.ts, so a missing EMAIL_FROM throws during import and stops the app booting rather than merely stopping its email. Terraform always writes the two into the same task definition revision, which is why the transport must never be flipped by hand in the ECS console. EMAIL_REPLY_TO is not DKIM-aligned and so carries an ordinary OSU mailbox, eecs-capstone@oregonstate.edu.

Object storage

Images live in an S3-compatible bucket (RustFS locally, AWS S3 in production).

docker compose up -d rustfs
npm run storage:init    # idempotent

Production note: configure the bucket as public-read at the bucket policy level on AWS, or run with S3_ENDPOINT set to your CDN base. Set VITE_STORAGE_PUBLIC_BASE to the customer-facing URL prefix.

AI-assisted proposal review

The project form can request per-field improvement suggestions, backed by AWS Bedrock. Set BEDROCK_MODEL_ID (and the relevant AWS credentials) to configure the model; the default is OpenAI GPT-5.6 Luna.

The call goes to the OpenAI-compatible Responses API on the bedrock-mantle endpoint, signed with SigV4 from the ambient AWS credentials, so no separate Bedrock API key is needed. BEDROCK_REASONING_EFFORT tunes how much the model thinks before answering. Embeddings still use bedrock-runtime.

Testing

This project uses Vitest.

npm run test

The auth and server surfaces have integration tests that hit the docker-compose Postgres:

npm run test:integration

Each test starts from a TRUNCATEd database, so they share a single fork and run serially. They read the schema as it exists, so run npm run db:migrate after pulling a migration or every one of them fails on the missing column.

Accessibility is checked separately, with Playwright and axe against the running app (the config starts npm run dev itself, or reuses one already listening on port 3000):

npm run test:accessibility

Pull requests run only the scans tagged @smoke (npm run test:accessibility:smoke); the whole suite runs from the dispatch-only Full Accessibility workflow.

TODO (future): integration tests currently run against the same database as dev and TRUNCATE every table before each test, which wipes dev data. Point them at a dedicated eecs_capstone_test database via a separate TEST_DATABASE_URL. See the Drizzle section of docs/QUIRKS.md for details.

Linting & Formatting

This project uses Ultracite (Biome under the hood).

npm run lint
npm run format
npm run check

Always run npm run check after finishing work and fix any issues before committing.

UI components (shadcn)

Add components using the latest version of shadcn:

npx shadcn@latest add button

Deploy to AWS

Production runs on AWS, provisioned with Terraform (infra/) and deployed via a one-click GitHub Actions workflow (.github/workflows/deploy.yml):

  • Compute: a single arm64 ECS Fargate task running the app's multi-stage Docker image.
  • Ingress: CloudFront is the only public entry point, serving HTTPS at capstone.eecs.oregonstate.edu (var.domain_name). Two DNS records live in the OSU-managed eecs.oregonstate.edu zone rather than in this Terraform configuration: one validating the ACM certificate, and one pointing the hostname at the CloudFront distribution. That is why the certificate is a read-only data lookup in infra/cloudfront.tf instead of a managed resource. CloudFront reaches an internal Application Load Balancer through a VPC origin, so the ALB itself has no public IP.
  • Data: a private RDS Postgres instance and a private S3 bucket for uploaded images, the latter served through its own CloudFront distribution via Origin Access Control.
  • Images/build: ECR holds built images; the deploy workflow builds natively on an arm64 GitHub-hosted runner (matching the Fargate architecture), pushes to ECR, runs migrations as a one-off ECS task, then rolls the service.
  • Secrets/config: app credentials come from the ECS task role (no static AWS keys); secrets live in Secrets Manager, non-secret config in the task definition and SSM.

See DEPLOYMENT.md for the full operational runbook (first-time setup through teardown) and infra/README.md for the Terraform specifics.

About

Project and inventory management for the EECS Capstone at Oregon State University

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages