# Project export: Apprentice Atlas

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: OpenAI Build Week
- Tagline: Discover apprenticeships near you and find a future that fits.
- Devpost: https://devpost.com/software/apprentice-atlas
- GitHub: https://github.com/estopia/apprentice-atlas
- Demo: https://testflight.apple.com/join/4xD3Ckd5
- Video: https://www.youtube.com/embed/ijnic6ery0A?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — ttvtimotheus (164 commits)

## Devpost submission (written by the team)

### Inspiration

“What am I supposed to do after school?” It is a dramatic line in our showcase video, but it reflects a real problem. Students are expected to make career decisions before they know which opportunities exist, what unfamiliar job titles mean, or what working in those roles would actually feel like. Traditional job boards assume that users already know what to search for. Their listings are often written in dense employer language, spread across different services, and designed around filling a vacancy rather than helping a young person understand their options. We wanted to build something that starts one step earlier. Apprentice Atlas helps students and people leaving university without a degree move from uncertainty to one concrete next step. Instead of beginning with a perfect job title, users can look at the opportunities around them, understand each role in plain language, assess whether it might fit them, and continue toward an actual application.

### What it does

Apprentice Atlas is a bilingual, map-first mobile app for discovering apprenticeships and early-career opportunities across Germany and the United Kingdom. Users can: Explore official opportunities on a native map or in a searchable list. Search by role, company, interest, location, and opportunity type. Use their current location or enter a location manually. Browse without creating an account. Open the original listing and official application source. Read a GPT-5.6 explanation written in clear, youth-friendly language. See balanced “Good if you…” and “Not so good if you…” guidance. Ask practical questions such as, “What would I actually do during a normal day?” Save promising opportunities after signing in. Track an application from interest and preparation through interview and offer. Add deadlines and interviews to the native calendar. Generate job-specific interview questions and a skill-gap analysis. Export application progress and account data. Use the complete product in English or German. The experience forms one continuous journey: That journey is what makes Apprentice Atlas different from a map with an AI summary attached. Discovery is only the beginning. The Home screen adapts to the user’s progress, saved opportunities become a shortlist, and My Atlas turns an interesting listing into an actionable application plan.

### How we built it

Apprentice Atlas is a native mobile application built with: Expo SDK 57 React Native and TypeScript Expo Router Apple Maps on iOS Supabase Auth, Postgres, Storage, Row Level Security, RPCs, and Edge Functions OpenAI Responses API with GPT-5.6 Official German and UK apprenticeship data sources EAS Build and TestFlight German opportunities are collected through Bundesagentur für Arbeit-hosted endpoints. UK opportunities use the official Find an apprenticeship Display Advert API v2. Data ingestion runs server-side and normalizes both providers into one shared job model. We deliberately chose official APIs instead of scraping websites. Anonymous users receive access only to active public listings. Saved jobs, application states, interview dates, and notes are protected by owner-only Supabase policies. Service-role credentials, provider payloads, synchronization records, and OpenAI credentials never ship in the mobile application. We created the project during Build Week, beginning with a written product design and implementation plan before building the first vertical slice. How GPT-5.6 powers the experience GPT-5.6 is not a decorative chatbot. It powers several focused product experiences: Rewriting dense vacancy text in simple language. Generating balanced fit guidance. Answering practical questions about a selected role. Creating job-specific interview questions. Comparing a voluntary user background with the requirements stated in the advert. Every request is grounded in the canonical job record fetched server-side. Outputs follow validated structured schemas, and the system is instructed to distinguish source facts from interpretation. When the advert does not contain enough information, the correct answer is not a confident guess—it is an honest unknown. The original vacancy and official source always remain available. Personal preparation requests use store: false, and the editable background draft remains on the user’s device rather than being written to the shared AI cache. How we collaborated with Codex We used Codex throughout the entire project as a persistent, repository-aware product and engineering partner—not as a one-shot prototype generator. Our workflow was: Define a product decision and its boundaries. Convert it into acceptance criteria and a testable specification. Implement a focused vertical slice with Codex. Test it in the browser and on a physical iPhone. Review the real result and provide direct product feedback. Iterate, add regression coverage, and review again. Codex accelerated work across the full stack: Translating product discussions into typed contracts, API boundaries, migrations, and implementation plans. Building Expo Router screens, native navigation, bilingual onboarding, authentication, map discovery, and application tracking. Implementing Supabase Edge Functions, database policies, source adapters, structured AI outputs, and server-side safeguards. Creating regression tests for authentication, RLS ownership, source normalization, ranking, navigation, application states, native fallbacks, and accessibility. Diagnosing EAS dependency failures, an iOS launch crash, missing Production environment variables, and a malformed Supabase key in a TestFlight build. Iterating on screens that initially felt sparse, text-heavy, generic, or too similar to a responsive web app. The product decisions remained human-led. We chose the audience, countries, official-data-only approach, bilingual experience, anonymous browsing boundary, optional location, dedicated Map tab, adaptive Home screen, and the requirement that AI admit uncertainty. Codex made those decisions dramatically faster to implement, inspect, test, and refine. The commit history documents that collaboration from the initial specification to a working TestFlight build. The final repository passes 362 automated tests, alongside TypeScript, Expo lint, Expo Doctor, static export checks, database permission tests, and physical-device verification. Challenges we faced Making different official APIs feel like one product The German and UK sources expose different fields, identifiers, location formats, detail structures, and update behavior. We built bounded server-side adapters and a normalized schema while preserving original source attribution and application links. Grounding AI without making it feel robotic A useful explanation must be easier to understand than the source while remaining faithful to it. We designed strict output schemas and prompts that allow interpretation but prohibit invented qualifications, working conditions, or employer promises. Building a real mobile experience Our early interface worked, but it felt closer to a web application than a finished native product. Testing on a physical iPhone exposed issues that browser testing could not: navigation labels, native module compatibility, map sensitivity, sheet layouts, safe areas, calendar behavior, and launch readiness. We repeatedly rebuilt the visual hierarchy and interaction patterns instead of accepting the first functional version. Shipping through EAS and TestFlight We encountered an out-of-sync npm lockfile, native framework compatibility issues, a launch-time crash, and a Production build with an incorrectly stored Supabase environment value. Codex helped trace each symptom to its actual cause, verify the fix, and add safeguards where appropriate. These problems reinforced an important lesson: a successful cloud build does not prove that a mobile product works. The final test must happen in the real distribution environment.

### What we learned

We learned that AI is most valuable when it reduces uncertainty at a specific moment—not when it is added everywhere. A teenager does not need an unrestricted career chatbot. They need answers grounded in the opportunity currently in front of them: What would I do? Could this suit me? What do I need to learn? What should I do next? We also learned that product quality comes from the loop between implementation and critique. Codex could generate a working screen quickly, but physical-device testing and direct design feedback determined whether that screen felt trustworthy, understandable, and genuinely native. Finally, we learned that official data, responsible AI boundaries, privacy, and polished UX are not separate concerns. Together, they determine whether a young person can confidently act on the information presented. What is next Apprentice Atlas will continue beyond Build Week. Our next priorities are broader official-source coverage, deeper regional partnerships, stronger accessibility, more personalized pathways, and collaboration with schools, training providers, public institutions, and youth organizations in Germany and the United Kingdom. The long-term goal is simple: No young person should need to know the name of their future career before they are able to discover it. Copyright © 2026 Estopia Engineering Ltd. Apprentice Atlas is source-available under the PolyForm Noncommercial License 1.0.0. Noncommercial use, study, modification, and distribution are permitted under its terms. Commercial use, commercial deployment, and commercial redistribution require a separate written license from Estopia Engineering Ltd. Third-party packages such as Expo and React Native, official-source content, trademarks, APIs, and provider data are not claimed as property of Estopia Engineering Ltd and remain subject to their respective licenses, terms, and ownership rights.

## README (from the GitHub repository)

# Apprentice Atlas

**Find your future nearby.**

> ## Test Apprentice Atlas on iPhone
>
> For the intended native experience, please test the app directly on an iPhone through TestFlight:
>
> **[Install Apprentice Atlas with TestFlight](https://testflight.apple.com/join/4xD3Ckd5)**

Apprentice Atlas is a bilingual, map-first mobile app that helps students and people leaving university without a degree turn uncertainty about their future into a concrete next step. It combines official apprenticeship listings from Germany and the United Kingdom with grounded GPT-5.6 guidance, native discovery, and a private application journey.

Built for the **Education** category of OpenAI Build Week 2026 with **Codex**, **GPT-5.6**, Expo, React Native, and Supabase.

## OpenAI Build Week submission at a glance

| Requirement | How Apprentice Atlas meets it |
| --- | --- |
| **Meaningful Codex use** | Codex was our repository-aware product and engineering partner from the initial specification through full-stack implementation, native iOS debugging, design iteration, testing, and TestFlight release hardening. The concrete workflow and human decisions are documented below. |
| **Meaningful GPT-5.6 use** | GPT-5.6 powers plain-language vacancy explanations, balanced fit guidance, grounded job Q&A, interview practice, and skill-gap analysis through protected Supabase Edge Functions. It is part of the core user journey rather than a standalone chatbot. |
| **Working, non-trivial product** | The bilingual Expo app combines official German and UK apprenticeship sources, native map discovery, authentication, private saved jobs, application tracking, deadlines, exports, and responsible AI safeguards. It has been exercised through a production TestFlight build on a physical iPhone. |
| **Engineering evidence** | The repository contains the product specification, implementation history, Supabase migrations and policies, Edge Functions, source adapters, structured AI schemas, and **362 passing automated tests**. |

Read the complete [Project Story](PROJECT_STORY.md), or jump directly to [how GPT-5.6 works](#gpt-56-in-the-product) and [how we collaborated with Codex](#how-we-collaborated-with-codex).

## The problem

Young people are often asked to choose a career before they understand what the available roles actually involve. Traditional job boards assume that users already know what to search for, present dense employer language, and stop at the application link.

Apprentice Atlas starts one step earlier: *What opportunities exist around me, what would I actually do there, and what should I do next?*

## What Apprentice Atlas does

- Discovers official apprenticeships and early-career opportunities on a native map or searchable list.
- Supports Germany and the United Kingdom in German and English.
- Uses location when permitted and provides a manual city/country fallback when it is not.
- Rewrites dense vacancy text in clear, youth-friendly language with GPT-5.6.
- Produces balanced **“Good if…”** and **“Not so good if…”** guidance grounded in the listing.
- Answers focused questions about a role without inventing facts the advert does not contain.
- Generates job-specific interview questions and an honest skill-gap view.
- Lets signed-in users save opportunities, track applications, manage interviews and deadlines, and export their progress.
- Keeps browsing open without an account while protecting personal data with authentication and row-level security.

## A complete path, not another job board

```text
Post-school uncertainty
        ↓
Official opportunities nearby
        ↓
Plain-language understanding
        ↓
Honest fit guidance
        ↓
A concrete next action
```

The map creates awareness, but the product does not end at discovery. Home adapts to the user's journey, saved roles become a shortlist, and the application tracker turns an interesting vacancy into an actionable plan.

## GPT-5.6 in the product

GPT-5.6 is a user-facing part of the shipped experience, not a decorative chatbot. The app calls it through protected Supabase Edge Functions using the canonical job record fetched server-side.

| Experience | GPT-5.6 contribution | Product safeguard |
| --- | --- | --- |
| In simple words | Rewrites dense vacancy text for a younger audience. | The original listing and official source remain visible. |
| Good if / Not so good if | Converts stated requirements and conditions into balanced fit guidance. | Structured output is validated and grounded in the advert. |
| Job Q&A | Answers practical questions about duties, requirements, and working life. | The answer explicitly admits when the source does not say. |
| Interview preparation | Creates role-specific practice questions and answer guidance. | The vacancy is fetched server-side and outputs follow a strict schema. |
| Skill-gap view | Compares a short user background with stated requirements. | It distinguishes evidence, learning areas, and positioning advice without inventing qualifications. |

OpenAI credentials never ship in the app. Personal preparation requests use `store: false`; the editable background draft stays on the device and is processed transiently for the requested analysis.

## How we collaborated with Codex

We used Codex as a persistent, repository-aware product and engineering partner from the first specification to physical-iPhone testing. The collaboration followed a repeatable loop:

1. Define the user problem and product boundary.
2. Turn the decision into a small, testable specification.
3. Implement a focused vertical slice with Codex.
4. Test in the browser and on a physical iPhone.
5. Give direct product and visual feedback, then iterate.
6. Run automated checks and independent review passes before merging.

Codex accelerated work across the stack:

- **Specification and architecture:** It translated product discussions into acceptance criteria, typed contracts, migrations, API boundaries, and implementation plans.
- **Native product development:** It implemented and connected Expo Router screens, map/list discovery, bilingual onboarding, authentication, Supabase data flows, and native device features.
- **Debugging:** It traced an EAS dependency failure to an out-of-sync lockfile, investigated a launch-time iOS crash, fixed overly sensitive map refreshes and an empty filter sheet, and helped make native modules reliable in the development client.
- **AI integration:** It built protected Edge Functions, structured-output schemas, grounding rules, quotas, and tests for explanations, Q&A, and preparation.
- **Quality:** It added regression coverage for source normalization, authentication return paths, RLS/RPC ownership, ranking, application states, accessibility, navigation, native fallbacks, and launch readiness.
- **Design iteration:** It used physical-device screenshots and native reference patterns to rebuild screens that initially felt sparse, text-heavy, or too much like a responsive website.

The key product decisions remained human-led: the audience, countries, official-data-only approach, bilingual experience, anonymous browsing boundary, optional location, map-as-a-dedicated-tab structure, and the requirement that AI admit uncertainty all came from deliberate product choices. Codex made those decisions faster to implement and easier to test; it did not replace them.

The commit history preserves that collaboration from the initial Expo project and written design through full-stack implementation, device debugging, UI critique, and release hardening. The more detailed engineering account is available in the [app README](apprentice-atlas/README.md#how-we-built-apprentice-atlas-with-codex-and-gpt-56).

## Architecture

```text
Expo + React Native mobile app
        │
        ├── anonymous discovery and local preferences
        ├── Supabase Auth (magic link and Sign in with Apple)
        └── Supabase Postgres with owner-only RLS
                    │
                    ├

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 197 recognized source files, 1070 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- Swift (language) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 216)

```
.DS_Store
.github/workflows/test.yml
.gitignore
apprentice-atlas/.claude/settings.json
apprentice-atlas/.env.example
apprentice-atlas/.gitignore
apprentice-atlas/.vscode/extensions.json
apprentice-atlas/.vscode/settings.json
apprentice-atlas/AGENTS.md
apprentice-atlas/app.config.ts
apprentice-atlas/app.json
apprentice-atlas/assets/ApprenticeAtlas.icon/icon.json
apprentice-atlas/assets/logo-loading.json
apprentice-atlas/CLAUDE.md
apprentice-atlas/docs/app-store-readiness.md
apprentice-atlas/docs/demo-script.md
apprentice-atlas/docs/superpowers/plans/2026-07-14-product-layer.md
apprentice-atlas/eas.json
apprentice-atlas/eslint.config.js
apprentice-atlas/LICENSE
apprentice-atlas/package.json
apprentice-atlas/README.md
apprentice-atlas/src/app/_layout.tsx
apprentice-atlas/src/app/(tabs)/_layout.tsx
apprentice-atlas/src/app/(tabs)/atlas.tsx
apprentice-atlas/src/app/(tabs)/explore.tsx
apprentice-atlas/src/app/(tabs)/favorites.tsx
apprentice-atlas/src/app/(tabs)/index.tsx
apprentice-atlas/src/app/(tabs)/map.tsx
apprentice-atlas/src/app/application/[jobId].tsx
apprentice-atlas/src/app/auth-callback.tsx
apprentice-atlas/src/app/auth.tsx
apprentice-atlas/src/app/filters.tsx
apprentice-atlas/src/app/job/[id].tsx
apprentice-atlas/src/app/legal/[document].tsx
apprentice-atlas/src/app/location.tsx
apprentice-atlas/src/app/onboarding.tsx
apprentice-atlas/src/app/prepare/[jobId].tsx
apprentice-atlas/src/app/search.tsx
apprentice-atlas/src/app/settings.tsx
apprentice-atlas/src/app/share/[jobId].tsx
apprentice-atlas/src/components/app-tabs.tsx
apprentice-atlas/src/components/app-tabs.web.tsx
apprentice-atlas/src/components/auth/auth-form.tsx
apprentice-atlas/src/components/company/company-brand-mark.tsx
apprentice-atlas/src/components/external-link.tsx
apprentice-atlas/src/components/hint-row.tsx
apprentice-atlas/src/components/home/home-job-card.tsx
apprentice-atlas/src/components/jobs/ai-explanation.tsx
apprentice-atlas/src/components/jobs/job-card.tsx
apprentice-atlas/src/components/jobs/job-filters.tsx
apprentice-atlas/src/components/jobs/job-qa.tsx
apprentice-atlas/src/components/jobs/job-share-card.tsx
apprentice-atlas/src/components/launch/launch-gate.tsx
apprentice-atlas/src/components/map/job-map.tsx
apprentice-atlas/src/components/map/job-map.web.tsx
apprentice-atlas/src/components/themed-text.tsx
apprentice-atlas/src/components/themed-view.tsx
apprentice-atlas/src/components/ui/app-icon.tsx
apprentice-atlas/src/components/ui/collapsible.tsx
apprentice-atlas/src/constants/theme.ts
apprentice-atlas/src/global.css
apprentice-atlas/src/hooks/use-auth.ts
apprentice-atlas/src/hooks/use-color-scheme.ts
apprentice-atlas/src/hooks/use-color-scheme.web.ts
apprentice-atlas/src/hooks/use-jobs.ts
apprentice-atlas/src/hooks/use-location.ts
apprentice-atlas/src/hooks/use-preferences.ts
apprentice-atlas/src/hooks/use-theme.ts
apprentice-atlas/src/lib/account.ts
apprentice-atlas/src/lib/ai.ts
apprentice-atlas/src/lib/application-flow.ts
apprentice-atlas/src/lib/applications.ts
apprentice-atlas/src/lib/atlas-next-action.ts
apprentice-atlas/src/lib/atlas.ts
apprentice-atlas/src/lib/auth-bound-client.ts
apprentice-atlas/src/lib/auth-presentation.ts
apprentice-atlas/src/lib/auth.ts
apprentice-atlas/src/lib/calendar-sync.ts
apprentice-atlas/src/lib/career-profile.ts
apprentice-atlas/src/lib/company-brand.ts
apprentice-atlas/src/lib/deadline-reminders.ts
apprentice-atlas/src/lib/device-preferences.ts
apprentice-atlas/src/lib/discovery-presentation.ts
apprentice-atlas/src/lib/discovery-state.ts
apprentice-atlas/src/lib/favorites.ts
apprentice-atlas/src/lib/filter-presentation.ts
apprentice-atlas/src/lib/home-presentation.ts
apprentice-atlas/src/lib/i18n.ts
apprentice-atlas/src/lib/job-detail-state.ts
apprentice-atlas/src/lib/job-filters.ts
apprentice-atlas/src/lib/job-presentation.ts
apprentice-atlas/src/lib/job-sharing.ts
apprentice-atlas/src/lib/jobs.ts
apprentice-atlas/src/lib/launch-readiness.ts
apprentice-atlas/src/lib/legal.ts
apprentice-atlas/src/lib/location.ts
apprentice-atlas/src/lib/map-clusters.ts
apprentice-atlas/src/lib/map-region.ts
apprentice-atlas/src/lib/native-feedback.ts
apprentice-atlas/src/lib/official-listing-url.ts
apprentice-atlas/src/lib/onboarding-destination.ts
apprentice-atlas/src/lib/onboarding-presentation.ts
apprentice-atlas/src/lib/preferences.ts
apprentice-atlas/src/lib/recent-searches.ts
apprentice-atlas/src/lib/request-guard.ts
apprentice-atlas/src/lib/review-prompt.ts
apprentice-atlas/src/lib/single-flight-gate.ts
apprentice-atlas/src/lib/supabase.ts
apprentice-atlas/src/types/jobs.ts
apprentice-atlas/src/types/styles.d.ts
apprentice-atlas/supabase/.env.example
apprentice-atlas/supabase/config.toml
apprentice-atlas/supabase/fixtures/preflight_source_provenance.sql
apprentice-atlas/supabase/functions/_shared/ai-http.ts
apprentice-atlas/supabase/functions/_shared/ai-schema.ts
apprentice-atlas/supabase/functions/_shared/ba-adapter.ts
apprentice-atlas/supabase/functions/_shared/filter-jobs.ts
apprentice-atlas/supabase/functions/_shared/normalize-job.ts
apprentice-atlas/supabase/functions/_shared/prompts.ts
[96 more files omitted for size]
```

### Dependencies

- apprentice-atlas/package.json: @expo/ui@~57.0.6, @lottiefiles/dotlottie-react@^0.13.5, @react-native-async-storage/async-storage@2.2.0, @react-native-community/datetimepicker@9.1.0, @supabase/supabase-js@^2.50.0, @types/react@~19.2.2, eslint@^9.0.0, eslint-config-expo@~57.0.0, expo@~57.0.6, expo-apple-authentication@~57.0.1, expo-calendar@~57.0.0, expo-constants@~57.0.3, expo-crypto@~57.0.1, expo-dev-client@~57.0.6, expo-device@~57.0.1, expo-font@~57.0.0, expo-glass-effect@~57.0.1, expo-haptics@~57.0.0, expo-image@~57.0.1, expo-linking@~57.0.3, expo-localization@~57.0.1, expo-location@~57.0.4, expo-notifications@~57.0.5, expo-print@~57.0.0, expo-router@~57.0.6, expo-sharing@~57.0.5, expo-splash-screen@~57.0.4, expo-status-bar@~57.0.1, expo-store-review@~57.0.0, expo-symbols@~57.0.1, expo-system-ui@~57.0.1, expo-web-browser@~57.0.1, lottie-react-native@~7.3.8, react@19.2.3, react-dom@19.2.3, react-native@0.86.0, react-native-gesture-handler@~2.32.0, react-native-maps@1.27.2, react-native-reanimated@4.5.0, react-native-safe-area-context@~5.7.0, react-native-screens@4.25.2, react-native-url-polyfill@^2.0.0, react-native-view-shot@5.1.0, react-native-web@~0.21.0, react-native-worklets@0.10.0, typescript@~6.0.3, vitest@^3.2.7

### Recent commits (newest first)

- feat: add private judge demo access
- Add TestFlight installation link to README
- Document Build Week submission details and production verification
- chore: update app version to 3.0.0
- Prepare v2 release and harden legal and navigation flows
- Adopt PolyForm Noncommercial license
- Replace MIT license with proprietary source-available notice
- docs: tell the Codex and GPT-5.6 build story
- feat: add company visuals across jobs
- style: enrich adaptive home experience
- fix: honor active discovery country on home
- docs: record adaptive home implementation plan
- fix: keep map view query lint-safe
- feat: add adaptive apprentice home
- feat: separate home search and map navigation
- feat: add adaptive home domain helpers
- style: separate job detail action cards
- fix: stabilize calendar and AI preparation
- feat: infer first-run locale from device
- style: reduce launch logo size

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

### PROJECT_STORY.md

```markdown
# Apprentice Atlas — Find Your Future Nearby

## Inspiration

“What am I supposed to do after school?”

It is a dramatic line in our showcase video, but it reflects a real problem. Students are expected to make career decisions before they know which opportunities exist, what unfamiliar job titles mean, or what working in those roles would actually feel like.

Traditional job boards assume that users already know what to search for. Their listings are often written in dense employer language, spread across different services, and designed around filling a vacancy rather than helping a young person understand their options.

We wanted to build something that starts one step earlier.

**Apprentice Atlas helps students and people leaving university without a degree move from uncertainty to one concrete next step.**

Instead of beginning with a perfect job title, users can look at the opportunities around them, understand each role in plain language, assess whether it might fit them, and continue toward an actual application.

## What it does

Apprentice Atlas is a bilingual, map-first mobile app for discovering apprenticeships and early-career opportunities across Germany and the United Kingdom.

Users can:

- Explore official opportunities on a native map or in a searchable list.
- Search by role, company, interest, location, and opportunity type.
- Use their current location or enter a location manually.
- Browse without creating an account.
- Open the original listing and official application source.
- Read a GPT-5.6 explanation written in clear, youth-friendly language.
- See balanced **“Good if you…”** and **“Not so good if you…”** guidance.
- Ask practical questions such as, “What would I actually do during a normal day?”
- Save promising opportunities after signing in.
- Track an application from interest and preparation through interview and offer.
- Add deadlines and interviews to the native calendar.
- Generate job-specific interview questions and a skill-gap analysis.
- Export application progress and account data.
- Use the complete product in English or German.

The experience forms one continuous journey:

```text
Post-school uncertainty
        ↓
Official opportunities nearby
        ↓
Plain-language understanding
        ↓
Honest fit guidance
        ↓
A concrete next action
```

That journey is what makes Apprentice Atlas different from a map with an AI summary attached. Discovery is only the beginning. The Home screen adapts to the user’s progress, saved opportunities become a shortlist, and My Atlas turns an interesting listing into an actionable application plan.

## How we built it

Apprentice Atlas is a native mobile application built with:

- **Expo SDK 57**
- **React Native and TypeScript**
- **Expo Router**
- **Apple Maps on iOS**
- **Supabase Auth, Postgres, Storage, Row Level Security, RPCs, and Edge Functions**
- **OpenAI Responses API with GPT-5.6**
- **Official German and UK apprenticeship data sources**
- **EAS Build a
[truncated — 6733 more characters]
```

### apprentice-atlas/CLAUDE.md

```markdown
@AGENTS.md

```

### apprentice-atlas/package.json

```
{
  "name": "apprentice-atlas",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "dependencies": {
    "@expo/ui": "~57.0.6",
    "@lottiefiles/dotlottie-react": "^0.13.5",
    "@react-native-async-storage/async-storage": "2.2.0",
    "@react-native-community/datetimepicker": "9.1.0",
    "@supabase/supabase-js": "^2.50.0",
    "expo": "~57.0.6",
    "expo-apple-authentication": "~57.0.1",
    "expo-calendar": "~57.0.0",
    "expo-constants": "~57.0.3",
    "expo-crypto": "~57.0.1",
    "expo-dev-client": "~57.0.6",
    "expo-device": "~57.0.1",
    "expo-font": "~57.0.0",
    "expo-glass-effect": "~57.0.1",
    "expo-haptics": "~57.0.0",
    "expo-image": "~57.0.1",
    "expo-linking": "~57.0.3",
    "expo-localization": "~57.0.1",
    "expo-location": "~57.0.4",
    "expo-notifications": "~57.0.5",
    "expo-print": "~57.0.0",
    "expo-router": "~57.0.6",
    "expo-sharing": "~57.0.5",
    "expo-splash-screen": "~57.0.4",
    "expo-status-bar": "~57.0.1",
    "expo-store-review": "~57.0.0",
    "expo-symbols": "~57.0.1",
    "expo-system-ui": "~57.0.1",
    "expo-web-browser": "~57.0.1",
    "lottie-react-native": "~7.3.8",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "react-native": "0.86.0",
    "react-native-gesture-handler": "~2.32.0",
    "react-native-maps": "1.27.2",
    "react-native-reanimated": "4.5.0",
    "react-native-safe-area-context": "~5.7.0",
    "react-native-screens": "4.25.2",
    "react-native-url-polyfill": "^2.0.0",
    "react-native-view-shot": "5.1.0",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.10.0"
  },
  "devDependencies": {
    "@types/react": "~19.2.2",
    "eslint": "^9.0.0",
    "eslint-config-expo": "~57.0.0",
    "typescript": "~6.0.3",
    "vitest": "^3.2.7"
  },
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "expo lint",
    "test": "vitest run"
  },
  "private": true
}

```

### apprentice-atlas/src/app/_layout.tsx

```typescript
import { DefaultTheme, router, Stack, ThemeProvider, useGlobalSearchParams, usePathname } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';

import { LaunchGate } from '@/components/launch/launch-gate';
import { Palette } from '@/constants/theme';
import { usePreferences } from '@/hooks/use-preferences';
import { t, useLocale } from '@/lib/i18n';
import { registerLocalNotificationHandling } from '@/lib/deadline-reminders';
import { getOnboardingGateParams } from '@/lib/onboarding-destination';
import { loadPreferences } from '@/lib/preferences';

void SplashScreen.preventAutoHideAsync().catch(() => undefined);

export default function RootLayout() {
  const pathname = usePathname();
  const { jobId, pendingAction, returnTo } = useGlobalSearchParams<{
    jobId?: string;
    pendingAction?: string;
    returnTo?: string;
  }>();
  const [locale] = useLocale();
  const { preferences, isHydrated: preferencesHydrated } = usePreferences();

  useEffect(() => {
    void loadPreferences();
  }, []);

  useEffect(() => {
    let disposed = false;
    let removeListener: (() => void) | undefined;

    void registerLocalNotificationHandling((route) => router.push(route))
      .then((remove) => {
        if (disposed) remove();
        else removeListener = remove;
      })
      .catch(() => undefined);

    return () => {
      disposed = true;
      removeListener?.();
    };
  }, []);

  useEffect(() => {
    if (!preferencesHydrated) return;
    if (!preferences.onboardingComplete && pathname !== '/onboarding') {
      router.replace({
        pathname: '/onboarding',
        params: getOnboardingGateParams(pathname, { jobId, pendingAction, returnTo }),
      });
    }
  }, [jobId, pathname, pendingAction, preferences.onboardingComplete, preferencesHydrated, returnTo]);

  const bootstrapReady = preferencesHydrated;

  return (
    <ThemeProvider value={{ ...DefaultTheme, colors: { ...DefaultTheme.colors, primary: Palette.blue, background: Palette.background, card: Palette.background, text: Palette.text, border: Palette.border } }}>
      <LaunchGate bootstrapReady={bootstrapReady} onboardingComplete={preferences.onboardingComplete} pathname={pathname}>
        <Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: Palette.background } }}>
          <Stack.Screen name="onboarding" options={{ gestureEnabled: false }} />
          <Stack.Screen name="(tabs)" />
          <Stack.Screen name="auth" options={{ presentation: 'modal', headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="auth-callback" options={{ gestureEnabled: false }} />
          <Stack.Screen name="search" options={{ headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="job/[id]" />
          <Stack.Screen name="share/[jobId]" options={{ headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="prepare/[jobId]" options={{ headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="settings" options={{ headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="legal/[document]" options={{ headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="application/[jobId]" options={{ presentation: 'formSheet', sheetAllowedDetents: [0.86, 1], sheetGrabberVisible: true, headerShown: true, headerShadowVisible: false, title: t(locale, 'application.sheetTitle'), contentStyle: { backgroundColor: Palette.surface } }} />
          <Stack.Screen name="filters" options={{ presentation: 'formSheet', sheetAllowedDetents: [0.85, 1], sheetGrabberVisible: true, headerShown: true, headerShadowVisible: false }} />
          <Stack.Screen name="location" options={{ presentation: 'formSheet', sheetAllowedDetents: [0.55, 0.85], sheetGrabberVisible: true, headerShown: true, headerShadowVisible: false }} />
        </Stack>
      </LaunchGate>
    </ThemeProvider>
  );
}

```

### apprentice-atlas/src/app/(tabs)/_layout.tsx

```typescript
import AppTabs from '@/components/app-tabs';

export default function TabsLayout() {
  return <AppTabs />;
}

```

### apprentice-atlas/supabase/functions/ai-qa/index.ts

```typescript
// @ts-ignore Supabase Edge Functions resolve npm: imports at runtime.
import { createClient } from 'npm:@supabase/supabase-js@2';
import { createQaHandler } from './handler.ts';

declare const Deno: { env: { get(name: string): string | undefined }; serve(handler: (request: Request) => Promise<Response>): void } | undefined;
const env = (name: string) => typeof Deno !== 'undefined' ? Deno.env.get(name) ?? '' : '';
export const handleQaRequest = createQaHandler({ env, createAdmin: (url, key) => createClient(url, key), fetcher: fetch });
if (typeof Deno !== 'undefined') Deno.serve(handleQaRequest);

```

### apprentice-atlas/supabase/functions/ai-explain/index.ts

```typescript
// @ts-ignore Supabase Edge Functions resolve npm: imports at runtime.
import { createClient } from 'npm:@supabase/supabase-js@2';
import { createExplainHandler } from './handler.ts';

declare const Deno: { env: { get(name: string): string | undefined }; serve(handler: (request: Request) => Promise<Response>): void } | undefined;
const env = (name: string) => typeof Deno !== 'undefined' ? Deno.env.get(name) ?? '' : '';
export const handleExplainRequest = createExplainHandler({ env, createAdmin: (url, key) => createClient(url, key), fetcher: fetch });
if (typeof Deno !== 'undefined') Deno.serve(handleExplainRequest);

```

### apprentice-atlas/supabase/functions/delete-account/index.ts

```typescript
// @ts-ignore Supabase Edge Functions resolve npm: imports at runtime.
import { createClient } from 'npm:@supabase/supabase-js@2';
import { createDeleteAccountHandler } from './handler.ts';

declare const Deno: { env: { get(name: string): string | undefined }; serve(handler: (request: Request) => Promise<Response>): void } | undefined;
const env = (name: string) => typeof Deno !== 'undefined' ? Deno.env.get(name) ?? '' : '';

export const handleDeleteAccount = createDeleteAccountHandler({
  env,
  createUserClient: (url, key) => createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } }),
  createAdminClient: (url, key) => createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } }),
});

if (typeof Deno !== 'undefined') Deno.serve(handleDeleteAccount);

```

### apprentice-atlas/supabase/functions/ai-prepare/index.ts

```typescript
// @ts-ignore Supabase Edge Functions resolve npm: imports at runtime.
import { createClient } from 'npm:@supabase/supabase-js@2';
import { createPrepareHandler } from './handler.ts';

declare const Deno: { env: { get(name: string): string | undefined }; serve(handler: (request: Request) => Promise<Response>): void } | undefined;
const env = (name: string) => typeof Deno !== 'undefined' ? Deno.env.get(name) ?? '' : '';

export const handlePrepareRequest = createPrepareHandler({
  env,
  createUserClient: (url, key) => createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } }),
  createAdmin: (url, key) => createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } }),
  fetcher: fetch,
});

if (typeof Deno !== 'undefined') Deno.serve(handlePrepareRequest);

```

### apprentice-atlas/supabase/functions/sync-jobs/index.ts

```typescript
// @ts-ignore Supabase Edge Functions resolve npm: imports at runtime.
import { createClient } from 'npm:@supabase/supabase-js@2';
import { createBaAdapter } from '../_shared/ba-adapter.ts';
import { createUkApprenticeshipAdapter } from '../_shared/uk-apprenticeship-adapter.ts';
import { SourceConfigurationError, type SourceAdapter } from '../_shared/source-adapter.ts';
import { isSyncRequestAuthorized } from '../_shared/sync-auth.ts';
import { runSync, type SyncRepository } from '../_shared/sync-runner.ts';
import { jobInsertPayload } from '../_shared/sync-helpers.ts';
import { parseSyncRequest } from '../_shared/sync-request.ts';

declare const Deno: { env: { get(name: string): string | undefined }; serve(handler: (request: Request) => Promise<Response>): void } | undefined;

const env = (name: string): string => typeof Deno !== 'undefined' ? Deno.env.get(name) ?? '' : '';
const secretKeys = (): string[] => {
  try {
    const parsed: unknown = JSON.parse(env('SUPABASE_SECRET_KEYS') || '{}');
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
      ? Object.values(parsed).filter((value): value is string => typeof value === 'string' && value.length > 0)
      : [];
  } catch {
    return [];
  }
};
const MAX_PAGES = Math.min(Math.max(Number(env('SYNC_MAX_PAGES') || 20), 1), 100);
const BA_MAX_PAGES = Math.min(Math.max(Number(env('BA_SYNC_MAX_PAGES') || 5), 1), 20);
const PAGE_DELAY_MS = Math.min(Math.max(Number(env('SYNC_PAGE_DELAY_MS') || 250), 0), 10_000);

type Provider = 'find-apprenticeship' | 'bundesagentur-fuer-arbeit';
type SupabaseLike = { from(table: string): any; rpc(name: string, params: Record<string, unknown>): any };

const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });

export function adapterFor(provider: Provider): SourceAdapter {
  return provider === 'find-apprenticeship' ? createUkApprenticeshipAdapter() : createBaAdapter();
}

function createSupabaseRepository(supabase: SupabaseLike): SyncRepository {
  return {
    async startRun(payload) {
      const result = await supabase.from('sync_runs').insert(payload).select('id').single();
      if (result.error || !result.data) throw new Error(`Unable to create sync run: ${result.error?.message ?? 'missing run id'}`);
      return { id: result.data.id };
    },
    async upsertJobSource(item, provider, fetchedAt) {
      const result = await supabase.rpc('upsert_job_source', {
        p_provider: provider,
        p_external_id: item.externalId,
        p_source_url: item.sourceUrl,
        p_raw_payload: item.rawRecord,
        p_job: jobInsertPayload(item, item.job.id, fetchedAt),
        p_fetched_at: fetchedAt,
      });
      if (result.error || !result.data?.[0]) throw new Error(`Unable to atomically upsert source ${item.externalId}: ${result.error?.message ?? 'missing canonical job'}`);
      return { jobId: result.data[0].job_id, inserted: Boolean(result.data[0].inserted) };
    },
    async expireStaleListings(provider, seenBefore) {
      const result = await supabase.rpc('expire_stale_source_jobs', { p_provider: provider, p_seen_before: seenBefore });
      if (result.error || typeof result.data !== 'number') throw new Error(`Unable to atomically expire stale listings: ${result.error?.message ?? 'missing expiration count'}`);
      return result.data;
    },
    async finishRun(runId, payload) {
      const result = await supabase.from('sync_runs').update(payload).eq('id', runId);
      if (result.error) throw new Error(`Unable to complete sync run: ${result.error.message}`);
    },
    async failRun(runId, payload) {
      const result = await supabase.from('sync_runs').update({ status: 'failed', ...payload }).eq('id', runId);
      if (result.error) {
        console.error('Unable to mark sync run failed', { runId, error: result.error });
        throw new Error(`Unable to mark sync run ${runId} failed: ${result.error.message}`);
      }
    },
  };
}

export async function handleSyncRequest(request: Request): Promise<Response> {
  if (request.method !== 'POST') return json({ error: { code: 'METHOD_NOT_ALLOWED', message: 'Use POST' } }, 405);
  const serviceRoleKey = env('SUPABASE_SERVICE_ROLE_KEY');
  const projectSecretKeys = secretKeys();
  if (!isSyncRequestAuthorized(request, { internalSecret: env('SYNC_INTERNAL_SECRET'), serviceRoleKey, secretKeys: projectSecretKeys })) return json({ error: { code: 'UNAUTHORIZED', message: 'Internal synchronization authorization required' } }, 401);
  try {
    const parsed = await parseSyncRequest(request);
    if (parsed.error) return json({ error: parsed.error }, 400);
    const body = parsed.body;
    const provider = body.provider ?? 'find-apprenticeship';
    if (provider !== 'find-apprenticeship' && provider !== 'bundesagentur-fuer-arbeit') return json({ error: { code: 'INVALID_PROVIDER', message: `Unsupported provider: ${String(provider)}` } }, 400);
    const adapter = adapterFor(provider);
    const supabaseUrl = env('SUPABASE_URL');
    const adminKey = projectSecretKeys[0] ?? serviceRoleKey;
    if (!supabaseUrl || !adminKey) throw new SourceConfigurationError('SUPABASE_URL and a Supabase server secret are required');
    const result = await runSync({ provider, sourceKey: `${provider}:${env('SYNC_SOURCE_CONFIGURATION') || 'default'}`, adapter, repository: createSupabaseRepository(createClient(supabaseUrl, adminKey)), maxPages: provider === 'bundesagentur-fuer-arbeit' ? BA_MAX_PAGES : MAX_PAGES, pageDelayMs: PAGE_DELAY_MS });
    return json(result);
  } catch (error) {
    const code = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' ? error.code : 'SYNC_ERROR';
    return json({ error: { code, message: error instanceof Error ? error.message : String(error) } }, error instanceof SourceConfigurationError ? 503 : 500);
  }
}

if (typeof Deno !== 'undefined') Deno.serve(handleSyncRequest);

```

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