Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Contributing

See [docs/contributing.md](./docs/contributing.md) for branching, scripts, tests, and schema-change rules.

Documentation index: [docs/README.md](./docs/README.md).
2 changes: 2 additions & 0 deletions GCP_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This guide explains how to set up your local environment to work with the GCP-hosted backend (Firebase App Hosting + Secret Manager).

Related: [docs/operations/deployment.md](./docs/operations/deployment.md), [docs/operations/environment.md](./docs/operations/environment.md), [docs/getting-started.md](./docs/getting-started.md).

## 1. Prerequisites

Install the following CLI tools:
Expand Down
217 changes: 49 additions & 168 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,188 +1,69 @@
# query

The central monorepo for club operations and digital infrastructure.
The central monorepo for Data Science at Georgia Tech club operations and digital infrastructure.

## Workspace layout
Two Next.js sites share one Postgres database and four internal packages. Club membership, events, bootcamp, and initiatives are modeled separately from hackathon editions, registration, teams, and judging — they share a database and touch nowhere.

| Path | Contents |
| --- | --- |
| `sites/mainweb` | Public club site |
| `sites/hacklytics2027` | Hacklytics 2027 event site (static export) |
| `packages/db` | Drizzle schema, client, seed script |
| `packages/api` | tRPC routers |
| `packages/auth` | NextAuth configuration |
| `packages/ui`, `packages/consts` | Shared components and constants |
| `tooling/*` | Shared eslint / tailwind / tsconfig |
**Documentation:** start at [`docs/README.md`](./docs/README.md).

## Database
## Workspace layout

Postgres, accessed through [Drizzle ORM](https://orm.drizzle.team). Production
runs on **Neon** (serverless Postgres, `us-west-2`, pooled endpoint); the
connection is made with `pg.Pool` in `packages/db/src/client.ts`, with SSL
required in production and a max pool size of 10.
| Path | Workspace | Role |
| --- | --- | --- |
| `sites/mainweb` | `web` | Public club site + authenticated portal (App Hosting) |
| `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, static export (Firebase Hosting) |
| `packages/api` | `@query/api` | tRPC routers, middleware, pricing |
| `packages/auth` | `@query/auth` | NextAuth (Google, GitHub, email codes) |
| `packages/db` | `@query/db` | Drizzle schema, client, membership rules |
| `packages/ui` | `@query/ui` | Shared React components |
| `tooling/*` | `@query/eslint-config`, `@query/prettier-config`, `@query/tailwind-config`, `@query/tsconfig` | Shared configs |

Configuration is a single environment variable:
## Quick start

```
DATABASE_URL=postgresql://<user>:<password>@<host>/<database>?sslmode=require
```bash
corepack enable
pnpm install
docker compose up -d
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/neondb \
pnpm --filter @query/db migrate:push
pnpm dev
```

`packages/db/src/client.ts` logs a warning and leaves `db` as `null` when the
variable is absent rather than throwing, so builds that never touch the database
still succeed.

### Schema

Schemas live in `packages/db/src/schemas/` and are re-exported from
`schemas/index.ts`. Drizzle picks them up via `schema: "./src/schemas/**/*.ts"`
in `drizzle.config.ts`.

| File | Tables |
| --- | --- |
| `auth.ts` | `user`, `account`, `session`, `verificationToken` |
| `members.ts` | `user_profile`, `member`, `membership_history` |
| `admins.ts` | `admin` |
| `hackathons.ts` | `hackathon`, `hackathon_team`, `hackathon_participant`, `hackathon_project`, `hackathon_event`, `hackathon_event_attendee` |
| `judge.ts` | `judge`, `judge_assignment`, `judging_project`, `judge_vote`, `judge_queue` |
| `initiatives.ts` | `project_leader`, `initiative`, `initiative_application` |
| `events.ts` | `event`, `event_check_in` |
| `stripe.ts` | `stripe_payment`, `user_account_link` |
| `security.ts` | `audit_logs` (+ `security_severity` enum) |
| `settings.ts` | `system_settings` |

Two entities anchor the graph:

- **`user`** — every identity-bearing table cascades from it: `account`,
`session`, `admin`, `user_profile`, `member`, `judge`, `event`,
`event_check_in`, `hackathon_team`, `hackathon_participant`,
`user_account_link`, and `stripe_payment.linked_user_id`.
- **`hackathon`** — every event-scoped table cascades from it: teams,
participants, projects, hackathon events, judges, judge assignments, judging
projects, judge queue, and maps.

Nearly all foreign keys are `onDelete: "cascade"`, so deleting a user or a
hackathon removes its dependent rows rather than orphaning them.

### Club and hackathon are separate

Two aspects share the database and touch nowhere:

- **Hackathon** — editions, registration, teams, project submission, judging.
Everything here hangs off a `hackathon` row.
- **Club** — `member`, `membership_history`, `event`, `event_check_in`,
`initiative`, its applications, and the `project_leader` role. Deliberately
**not** scoped to a hackathon. A club project runs whenever somebody leads
one, and leading is a standing appointment rather than a yearly re-grant.
Nothing in this half is ever judged; judges only score `hackathon_project`.

The two halves no longer cross. `member` used to be `unique(user_id,
hackathon_id)`, which welded a paid year to an edition: the day the next
hackathon opened, every paying member read as a non-member. It is now
`unique(user_id)` and a membership is defined entirely by its own dates, with
`membership_history` recording which years somebody held one. The club half
therefore works with no hackathon in the database at all.

#### One-off step — only for a database that already has the edition-scoped tables

**Check first:**

```sql
SELECT to_regclass('public.project_leader');
```
- Club site + portal: [http://localhost:3001](http://localhost:3001)
- Hacklytics 2027: [http://localhost:3000](http://localhost:3000)

If that returns `NULL`, this database has never had the club tables. Skip
everything below — `migrate:push` simply creates them in the current shape, and
the statements here would error on tables that do not exist.

If it returns a table name, `migrate:push` cannot work the change out on its
own. `project_leader` moved from `unique(user_id, hackathon_id)` to
`unique(user_id)`, so anybody appointed in more than one edition has more than
one row; drizzle-kit fails building the new index partway and leaves the schema
half-applied. Run this against that database **once, before** the push. Every
statement is guarded, so it is safe to re-run.

```sql
BEGIN;

-- Collapse duplicate leader appointments to one row per person. Keeps the
-- oldest row, so created_at still reads as when they were first appointed, and
-- keeps the role switched on if ANY of their rows was active — dropping an
-- active appointment here silently locks a leader out of their own initiatives.
WITH ranked AS (
SELECT
id,
user_id,
bool_or(is_active) OVER (PARTITION BY user_id) AS any_active,
row_number() OVER (PARTITION BY user_id ORDER BY created_at ASC, id ASC) AS rn
FROM project_leader
)
UPDATE project_leader AS pl
SET is_active = ranked.any_active
FROM ranked
WHERE pl.id = ranked.id
AND ranked.rn = 1
AND pl.is_active IS DISTINCT FROM ranked.any_active;

DELETE FROM project_leader
WHERE id IN (
SELECT id FROM (
SELECT
id,
row_number() OVER (PARTITION BY user_id ORDER BY created_at ASC, id ASC) AS rn
FROM project_leader
) dupes
WHERE rn > 1
);

-- Drop the edition columns and everything hanging off them.
ALTER TABLE project_leader
DROP CONSTRAINT IF EXISTS unique_project_leader_per_hackathon;
DROP INDEX IF EXISTS project_leader_hackathon_id_idx;
ALTER TABLE project_leader DROP COLUMN IF EXISTS hackathon_id;

DROP INDEX IF EXISTS initiative_hackathon_id_idx;
ALTER TABLE initiative DROP COLUMN IF EXISTS hackathon_id;

-- The constraint the new schema expects. Added here rather than left to push,
-- so a collision surfaces inside this transaction where it rolls back.
ALTER TABLE project_leader
DROP CONSTRAINT IF EXISTS unique_project_leader;
ALTER TABLE project_leader
ADD CONSTRAINT unique_project_leader UNIQUE (user_id);

COMMIT;
```
Full setup, env vars, and first-admin bootstrap: [docs/getting-started.md](./docs/getting-started.md).

Initiatives themselves are untouched. Rows that were invisible because they
belonged to a past edition become visible again — that is the point, they were
club projects an edition rollover hid. Archive any that should not come back
from the leader screen afterwards.
```bash
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```

### Working with the schema
## Architecture (short)

```bash
pnpm --filter @query/db migrate:push # push schema changes to DATABASE_URL
pnpm --filter @query/db migrate:generate # emit SQL into packages/db/drizzle
pnpm --filter @query/db studio # Drizzle Studio
pnpm --filter @query/db db:seed # scripts/seed.ts
```
hacklytics2027 (static) ──interest CTA──► mainweb portal
@query/api ◄──session──► @query/auth
@query/db → Neon / local Postgres
```

The project is **push-based**: `packages/db/drizzle/meta/_journal.json` has no
entries and there are no generated `.sql` files, so schema changes are applied
directly with `migrate:push` rather than through a migration history. If you
want reviewable migrations, switch to `migrate:generate` and commit the output.
The portal is a route group inside `sites/mainweb`, not a separate app. The event site does not query the database.

### Local database
Details: [docs/architecture.md](./docs/architecture.md). Schema and the club/hackathon split: [docs/packages/db.md](./docs/packages/db.md).

`docker-compose.yml` brings up a local Postgres with the same database name as
Neon, so only `DATABASE_URL` changes between the two:
## Deploy

```bash
docker compose up -d
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/neondb \
pnpm --filter @query/db migrate:push
```
| Surface | Platform | Config |
| --- | --- | --- |
| `web` | Firebase App Hosting / Cloud Run | `apphosting.yaml` |
| `hacklytics2027` | Firebase Hosting target `hacklytics` | `firebase.json` |

GCP project: `dsgt-website`. Local secret sync: [GCP_SETUP.md](./GCP_SETUP.md). Operations: [docs/operations/deployment.md](./docs/operations/deployment.md).

## License

It publishes on host port **5433** to avoid colliding with a system Postgres,
and has a `pg_isready` healthcheck so `migrate:push` is not run against a
container that is still starting.
Apache License 2.0. See [LICENSE](./LICENSE).
37 changes: 37 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Documentation

This folder is the reference for **query**, the Data Science at Georgia Tech (DSGT) monorepo for club operations and digital infrastructure.

Start here, then jump to the page that matches the work you are doing.

| Document | What it covers |
| --- | --- |
| [Getting started](./getting-started.md) | Prerequisites, local Postgres, env vars, first `pnpm dev` |
| [Architecture](./architecture.md) | How the two sites and four packages fit together |
| [Contributing](./contributing.md) | Branches, scripts, tests, and review expectations |
| [Environment variables](./operations/environment.md) | Every env var the process actually reads |
| [Deployment](./operations/deployment.md) | Firebase App Hosting, Firebase Hosting, GCP secrets |
| [CI/CD](./operations/ci-cd.md) | GitHub Actions, Dependabot, branch automation |
| [Security](./operations/security.md) | Auth gates, rate limits, CSP, input scrubbing |
| [Testing](./operations/testing.md) | Vitest, Playwright, and what each suite protects |
| [Glossary](./glossary.md) | Club vs hackathon vocabulary |

## Packages

| Document | Workspace | Role |
| --- | --- | --- |
| [API](./packages/api.md) | `@query/api` | tRPC routers, middleware, pricing |
| [Auth](./packages/auth.md) | `@query/auth` | NextAuth, providers, mailer |
| [Database](./packages/db.md) | `@query/db` | Drizzle schema, client, membership rules |
| [UI](./packages/ui.md) | `@query/ui` | Shared React components and styles |

## Sites

| Document | Workspace | Role |
| --- | --- | --- |
| [Main website](./sites/mainweb.md) | `web` | Public club site plus the authenticated portal |
| [Hacklytics 2027](./sites/hacklytics2027.md) | `hacklytics2027` | Static event marketing site |

## Tooling

Shared ESLint, Prettier, Tailwind, and TypeScript configs live under [`tooling/`](./tooling.md).
Loading
Loading