A SvelteKit application for tracking apprenticeship progress, integrated with Airtable.
npm installCreate a .env.local file with your credentials:
AIRTABLE_API_KEY=your_api_key
AIRTABLE_BASE_ID=your_base_id
RESEND_API_KEY=your_resend_key
RESEND_FROM_EMAIL=App Name <[email protected]>
AUTH_SECRET=your_auth_secret_min_32_chars
The app uses magic link authentication with separate login flows for staff and students:
| Type | Login Page | Validates Against | Default Redirect |
|---|---|---|---|
| Staff | /admin/login |
Staff table (collaborator email) | /admin |
| Student | /login |
Apprentices table (learner email) | / |
- User enters email on the appropriate login page
- Server validates email exists in the corresponding Airtable table
- JWT token generated (15-minute expiry) and emailed via Resend
- User clicks link → token verified → session cookie set (90-day expiry)
- User redirected to appropriate dashboard
The app uses SvelteKit hooks (src/hooks.server.ts) for centralized route protection:
/admin/*→ Staff only (redirects students to/)/checkin→ Any authenticated user/login,/admin/login→ Redirects away if already authenticated
- Add them as a collaborator in the Airtable workspace
- Add a record in the Staff - Apprentice Pulse table, selecting their collaborator profile
- They can now log in at
/admin/loginusing their collaborator email
npm run devIn development, you can test login using the UI or via curl:
- Start the dev server:
npm run dev - Go to
/login(students) or/admin/login(staff) - Enter an email that exists in the appropriate Airtable table
- Check your email for the magic link
# Staff login
curl -X POST http://localhost:5173/api/auth/staff/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
# Student login
curl -X POST http://localhost:5173/api/auth/student/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'Then click the magic link in your email, or manually visit:
http://localhost:5173/api/auth/verify?token=YOUR_TOKEN
Note: The verify step must be done in the browser so the session cookie is set correctly.
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Build for production |
npm run test |
Run tests |
npm run lint |
Run ESLint |
npx tsx scripts/fetch-schema.ts |
Fetch Airtable schema |
npx tsx scripts/test-airtable.ts |
Test Airtable connection |
- SvelteKit
- TailwindCSS
- Airtable
- Vitest + Playwright
We reference Airtable tables and fields by their IDs rather than names to prevent breakage when users rename things.
Use returnFieldsByFieldId: true in select queries to access fields by ID:
const records = await table
.select({
filterByFormula: `{Field Name} = "value"`,
returnFieldsByFieldId: true,
})
.all();
const value = record.get('fldXXXXXXXXXXXXXX'); // field IDLimitation: filterByFormula still requires field names, not IDs. This is an Airtable API limitation.
The following field names are used in filterByFormula queries. Renaming these fields in Airtable will break the app:
| Table | Field Name | Used In |
|---|---|---|
| Apprentices | Learner email |
findApprenticeByEmail() |
| Cohorts | FAC Cohort |
getApprenticesByFacCohort() |
| Events | FAC Cohort |
listEvents() cohort filter |
| Events | Date Time |
listEvents() date range filter |
When adding new functionality or when Airtable schema changes, use the schema fetch script to get the correct table and field IDs:
npx tsx scripts/fetch-schema.tsThis script:
- Connects to your configured Airtable bases
- Presents an interactive prompt to select which tables to include
- Outputs a timestamped markdown file (e.g.,
scripts/schema-2025-01-15-10-30-00.md) with all table IDs and field IDs
Always use this script to get IDs rather than copying them manually from the Airtable UI. This ensures accuracy and provides documentation of the schema at that point in time.
The generated schema file can be used to update src/lib/airtable/config.ts with new field IDs.
Event types are defined in src/lib/types/event.ts. The EventType union currently includes:
Regular classWorkshopHackathon
If new event types are added in Airtable's "Select" field, update this type definition to match.