Capstone progress: 0 / 15Download guide

Coding with AI on a Mac

A novice-to-intermediate course using an applicant tracking system

Before the training: how an experienced developer thinks

An experienced developer starts with the ATS's users, data and hiring rules: who can see each applicant, which stage changes are allowed, and what happens when an email or upload fails. They choose a small, maintainable stack—one modular application, PostgreSQL, private résumé storage, established authentication and background jobs—then build and test one complete workflow. A strong technical package also includes reviewed migrations, automated checks, deployment instructions, monitoring and proven backup recovery. The goal is a system another developer can understand, operate and safely change.

Prepared October 8, 2026. Estimated study time: 24–36 hours across six weeks. Work at your own pace; repeat labs until you can explain each step. All terminal examples target macOS Terminal with zsh, not Windows PowerShell.

This is a training course and implementation blueprint, not a finished ATS or an automatic AWS deployment. Use fictional applicants throughout. Examples labeled templates require the application or cloud resources described beside them. No GitHub repository or AWS resources have been created by this guide.

Course map

LessonWhat you learnEvidence you can do it
1Mac terminal and tool setupFind your project and check installed tools
2GitHub interface and a new repositoryCreate a private repository and push a README
3Repository contentsOrganize code, configuration, documentation and tests
4Everyday Git and pull requestsMake, review and merge a small feature
5Coding with a terminal AI assistantGive a bounded task and verify its changes
6ATS architecture and first featureTrace browser → server → database
7Docker and local PostgreSQLStart a database and connect to it
8SQL, migrations and recoveryCreate related tables and restore a backup
9Applicant and hiring workflowsDefine transitions, retries and audit events
10Security and applicant privacyDemonstrate access denial and safe uploads
11GitHub Actions and deliveryReject a broken change before deployment
12AWS and operational readinessExplain a deployment, rollback and recovery

Suggested schedule: lessons 1–2 in week one; 3–4 in week two; 5–6 in week three; 7–8 in week four; 9–10 in week five; 11–12 and the capstone in week six.

1. Get comfortable in Mac Terminal

Goal: know where commands run and which files they affect.

Open Terminal through Spotlight: press Command–Space, type Terminal, press Return. A prompt ending in % is normal. Do not type the prompt itself. Run one command at a time while learning. Text inside code blocks is meant for Terminal unless labeled SQL, YAML or a file's contents.

CommandMeaning
pwdShow your current folder
ls -laList files, including hidden files
cd ~/DeveloperMove into a folder
cd ..Move up one folder
mkdir -p ~/Developer/ats-trainingCreate folders
touch README.mdCreate an empty file if missing
cat README.mdPrint a small text file
open .Open the current folder in Finder
nano README.mdEdit a text file in Terminal
which gitLocate an installed command

In nano, Control–O saves, Return confirms the name, and Control–X exits. Control–C stops a foreground server. Tab completes paths. The up arrow recalls previous commands. A quoted path such as cd "~/Developer/My Project" will not expand ~; use cd "$HOME/Developer/My Project" instead.

Create your practice folder:

sh
mkdir -p ~/Developer/ats-training
cd ~/Developer/ats-training
pwd

Install Apple's command-line tools if Git is unavailable:

sh
xcode-select --install
git --version

Install Homebrew from its official instructions, then follow its displayed shell setup instructions. Apple Silicon and Intel Macs can use different installation paths; do not guess the path.

With Homebrew ready:

sh
brew install git gh node@24 awscli

Follow Homebrew's instructions for placing node@24 on your PATH. The training uses Node 24 as a fixed baseline; match the version in your project and CI. Install Docker Desktop for your Mac's chip, open the application, and wait for the engine to start. See Docker's Mac installation guide.

sh
git --version
gh --version
node --version
npm --version
docker version
docker compose version
aws --version

Lab: create notes.md, write what pwd means, save it, and display it with cat. Success means you can find the same file in Finder and Terminal.

Mac productivity: commands and shortcuts

Goal: move quickly between Terminal, Finder, your editor and GitHub. Command (⌘) and Control (⌃) are different keys.

Everyday Mac shortcuts

ShortcutAction
Command–SpaceSpotlight: launch an app or find a file
Command–TabSwitch applications
Command–backtickSwitch windows in the current app
Command–C / Command–VCopy / paste
Command–ZUndo
Command–SSave
Command–FFind text
Command–W / Command–QClose window / quit app
Command–commaCurrent app settings
Control–Command–QLock screen
Shift–Command–G in FinderJump to a typed folder path
Command–Up Arrow in FinderParent folder
Space in FinderQuick Look preview
Shift–Command–period in FinderToggle hidden files
Shift–Command–4Capture selected screen area
Shift–Command–5Screenshot/recording controls

Shortcuts can vary with application or configured keyboard settings. See Apple's shortcut reference and screenshot guide.

Faster Terminal use

KeyAction
TabComplete a path or command
Up / Down ArrowRecall command history
Control–A / Control–EMove to start / end of shell command line
Control–URemove text before cursor in the usual zsh editing mode
Control–KRemove text after cursor
Control–RSearch command history in the usual shell configuration
Control–CInterrupt the running foreground command
Command–TNew Terminal tab
Command–plus / Command–minusAdjust text size

Shell-editing keys depend on your configuration. Copy in Terminal with Command–C; Control–C interrupts a process. See Apple's Terminal shortcuts.

Mac-specific terminal command card

sh
# Open your working folder or a document in its default app.
open .
open README.md

# Open a web page or launch an installed app.
open https://github.com
open -a "Visual Studio Code" .

# Copy harmless text or a file's contents to the Mac clipboard.
printf 'Hello from Terminal' | pbcopy
cat README.md | pbcopy

# Save clipboard contents to a new notes file; > replaces that file if it exists.
pbpaste > clipboard-notes.txt

# Search filenames inside this project.
find . -name '*candidate*' -not -path './node_modules/*'

# Search indexed Mac files by filename.
mdfind -name 'ats-training'

# Inspect disk space and the size of this project.
df -h
du -sh .

# Keep the Mac awake while a foreground command runs.
# Use only once the project's build script exists.
caffeinate -i npm run build

# Read the built-in manual; press q to exit.
man open

Spotlight indexing determines what mdfind can locate. For fast code searches, optionally install ripgrep with brew install ripgrep, then use rg 'organization_id' src and rg --files. Restrict searches to relevant folders and avoid printing secrets into terminal logs.

Safe aliases for frequent commands

An alias gives a short name to a longer command. Edit ~/.zshrc with nano ~/.zshrc and add:

sh
alias croot='cd ~/Developer/ats-training'
alias gs='git status'
alias gd='git diff'
alias gl='git log --oneline -10'
alias dps='docker compose ps'

Open a fresh Terminal tab or run source ~/.zshrc. Keep commit, push, deletion and deployment commands explicit while learning. Aliases should save typing without hiding consequential actions.

Five-minute drill: launch Terminal through Spotlight; type croot; open the folder with open .; preview README in Finder; return to Terminal; run gs; copy the README with pbcopy; paste it into a note. Then practice recalling and editing a harmless command without retyping it.

2. Learn the GitHub interface and create a repository

Goal: connect a folder on your Mac to a private repository on GitHub.

Git records changes locally. GitHub hosts repositories and adds reviews, issues, permissions and automation. A commit is a saved checkpoint; a push sends checkpoints to GitHub. A private repository limits visibility but does not make it a safe place for passwords or applicant records.

GitHub interface tour

GitHub areaWhat you do thereATS example
CodeBrowse files, select branches, copy clone URLRead the API and README
IssuesDescribe work, defects and acceptance criteria“Recruiter can move application to interview”
Pull requestsReview a branch before mergingCheck authorization and tests
ActionsSee automated build and test resultsFind why a test failed
ProjectsOrganize issues into a work boardBacklog → Ready → Doing → Review → Done
SecurityReview available security features and findingsDependency alerts and exposed secrets
SettingsManage access, rules, environments and configurationRequire checks before merging

Availability of some security and protection features depends on repository visibility and account plan. Interface labels may change. The underlying GitHub branch and pull-request workflow remains the skill to learn.

Route A: browser first

  1. Sign in to GitHub and enable account MFA.
  2. Use the plus menu → New repository.
  3. Choose the owner and name ats-training; select Private.
  4. Add a README and a Node .gitignore. Choose a license only if you intend to grant reuse rights; a proprietary internal project may not need an open-source license.
  5. Create the repository. Select Code → HTTPS and copy its URL.
  6. Authenticate and clone below. Replace YOUR-USERNAME before running.
sh
gh auth login
gh auth setup-git
cd ~/Developer
git clone https://github.com/YOUR-USERNAME/ats-training.git
cd ats-training
git status

If you already created the empty practice folder in lesson 1, rename it in Finder or choose a different clone destination before this route. Git cannot clone into a nonempty folder.

Route B: existing local folder

Use this instead of Route A. Configure your real name and your GitHub-approved email, or its provided no-reply email.

sh
cd ~/Developer/ats-training
git config --global user.name "Your Name"
git config --global user.email "YOUR-GITHUB-EMAIL"
git init -b main
printf '# ATS Training\n\nPractice project using fictional data.\n' > README.md
printf '.env\n.env.*\n!.env.example\nnode_modules/\n.secrets/\nbackups/\n.DS_Store\n' > .gitignore
git add README.md .gitignore
git commit -m "docs: initialize ATS training repository"
gh auth login
gh auth setup-git
gh repo create ats-training --private --source=. --remote=origin --push
gh repo view --web

If configuring Git identity was skipped in Route A, do it before your first commit. The browser should show your README. The CLI route is documented in GitHub CLI repository creation. For an existing local repository added through the website, create an empty remote without a README, then add its URL and push; see adding local code to GitHub.

Lab: change the README, commit it, push it, and inspect the commit in GitHub. Explain why saving a file alone does not update GitHub.

3. What belongs inside the repository?

Goal: make the project understandable and reproducible for another developer.

Start with one repository and one deployable application, a modular monolith. Separate modules inside the code before considering separately deployed services. The following is a target structure, not a claim these files already exist:

text
ats-training/
  README.md
  AGENTS.md
  .gitignore
  .dockerignore
  .env.example
  package.json
  package-lock.json
  Dockerfile
  compose.yaml
  src/
    web/
    server/
      auth/
      jobs/
      candidates/
      applications/
      workflows/
  db/
    migrations/
    seed/
  tests/
    unit/
    integration/
    e2e/
  docs/
    architecture.md
    permissions.md
    threat-model.md
    runbook.md
    decisions/
  infra/
    README.md
  .github/
    workflows/ci.yml
    pull_request_template.md
IncludeWhy
READMEPurpose, prerequisites, install/start/test commands, configuration and troubleshooting
Dependency manifest and lockfileRecord dependencies and reproducible resolved versions
.env.exampleShow configuration names and harmless placeholders
MigrationsReview and reproduce database changes
Synthetic seed dataDemonstrate features without personal data
TestsProtect behavior, permission boundaries and workflows
Infrastructure definitionsReview and recreate cloud configuration
Architecture decisionsExplain why a choice was made and its tradeoffs
RunbookDescribe deployment, rollback, alerts and recovery

Keep passwords, AWS keys, real résumés, production exports, logs containing personal data, database volumes and generated dependencies out of Git. Git's history retains previously committed contents; deleting a secret in a later commit does not revoke it. If exposed, revoke or rotate it first, then clean history and review access.

Use this .gitignore baseline and adapt generated-build folders to your framework:

gitignore
node_modules/
.env
.env.*
!.env.example
.secrets/
backups/
uploads/
coverage/
dist/
.next/
.DS_Store
*.log

.dockerignore should exclude .git, node_modules, .env, .env.*, .secrets, backups, uploads and build outputs you do not intend to copy. This prevents local sensitive files from entering the image build context.

Create the initial folders:

sh
mkdir -p src/web src/server db/migrations db/seed
mkdir -p tests/unit tests/integration tests/e2e docs/decisions infra .github/workflows

Git does not track empty directories. Add meaningful files as you work. Document the package scripts before using them: dev, lint, typecheck, test, test:integration, test:e2e, build, start, db:migrate, and db:seed. These are project conventions, not built-in npm commands.

Lab: write a README that lets a second person start the project without asking you where the configuration lives.

4. Daily Git: edit, review, commit, push

Goal: keep changes small enough to understand and recover.

CommandWhat it does
git statusShow branch and changed/staged files
git diffShow changes not yet staged
git add README.mdStage one file for the next commit
git diff --stagedReview exactly what the commit will contain
git commit -m "message"Save the staged checkpoint locally
git pushSend commits to the configured remote branch
git fetch originDownload remote history without merging it
git pull --ff-onlyUpdate when a simple forward update is possible
git switch -c feat/candidate-listCreate and enter a feature branch
git log --oneline -8Inspect recent checkpoints
git remote -vShow remote repository addresses

A complete feature flow, after the application and scripts exist:

sh
git switch main
git pull --ff-only
git switch -c feat/candidate-list
# Edit the files and run the checks defined in package.json.
git status
git diff
git add src tests
git diff --staged
git commit -m "feat: add recruiter candidate list"
git push -u origin feat/candidate-list
gh pr create --base main --title "Add recruiter candidate list" --body "Adds the candidate list with organization-scoped access and tests."

-u remembers the upstream; later pushes can use git push. Stage only paths relevant to the feature and inspect them. In the GitHub pull request, read Files changed, check automated results, request review, and merge when ready. Then update your local main branch.

For a docs-only lab, stage README.md instead of src tests. No application is required to learn the flow.

Recover without guessing

To unstage a file without losing its edits:

sh
git restore --staged README.md

To preserve unfinished work while switching tasks:

sh
git stash push -m "unfinished candidate list"
git stash list
git stash pop

To undo a committed change through a new commit, first find its identifier with git log, then run git revert COMMIT_ID. Replace that placeholder. Resolve any conflict, run checks, and push the new commit.

If push is rejected, run git fetch origin and inspect the history; someone else may have changed the branch. Do not reflexively force-push. If a merge has conflicts, inspect git status, edit the conflict markers, stage resolved files and finish the merge. git merge --abort abandons a merge in progress; it is not a general undo command. Avoid git reset --hard and git clean -fd while learning because they can discard local work.

Lab: open a README pull request, read its diff, merge it, and update local main. Success means you can identify local edits, staged edits, a commit, and a remote branch separately.

5. Use AI from the terminal

Goal: make AI a coding partner while keeping responsibility for correctness.

Codex CLI is one example; the task/review loop also applies to other terminal coding assistants. Use the installation method shown in the official Codex CLI documentation, then run codex from the repository. The first run offers available sign-in methods. Review its permissions and create Git checkpoints around tasks.

sh
cd ~/Developer/ats-training
git status
git switch -c feat/first-ai-task
codex

A coding assistant may read files, edit code and execute commands under its configured permissions. Verify your organization's rules and provider settings before sharing proprietary code. Use synthetic data, and never include credentials or applicant records in prompts.

Give the assistant a clear contract

Example prompt:

Read the README and existing code. Explain the current architecture before editing. Implement a candidate-list page using fictional data. Keep this change limited to the candidate list. Use the existing framework and dependencies. Include loading, empty and error states. Do not add external services. After editing, run the documented checks and report changed files, test results and unresolved limitations. Explain the important code at a novice level.

For a later security task:

Implement the application stage-transition endpoint. Derive organization and permissions from the authenticated server-side session. Reject unauthorized transitions and cross-organization access. Save the stage update and audit event in one transaction. Add tests for allowed, denied and repeated requests. Do not deploy or run production migrations.

Ask for one vertical feature at a time. Avoid “build the whole enterprise ATS” because it is hard to inspect and easy to leave gaps.

Add project instructions

Create AGENTS.md with concise repository-specific guidance:

text
Project: training ATS using synthetic data only.
Keep the implementation a modular monolith.
Follow the existing framework and dependency versions.
Enforce organization and role checks on the server.
Do not place secrets, applicant records or uploads in Git.
Use reviewed migrations for schema changes.
Run the checks documented in README; report what actually ran.
Do not claim a test passed if it was skipped or unavailable.
Explain behavior, risks and changed files after each task.

Verify each AI change

  1. Ask it to explain the plan and the files it will change.
  2. Let it implement a bounded task on a branch.
  3. Inspect git diff; look for new dependencies, credentials, removed tests, relaxed permissions and unrelated edits.
  4. Run tests yourself and exercise the feature in the browser.
  5. Ask a follow-up review question: “What could allow another organization to see this data?”
  6. Commit only changes you can explain. Review is still necessary even when the same AI wrote tests.

Lab: have AI improve the README, then manually compare the documented commands with the actual files. Success means you can catch at least one unsupported assumption or explicitly confirm none were found.

6. Design a strong technical package for an ATS

Goal: choose a maintainable architecture rather than accumulate tools.

My suggested teaching stack is TypeScript with a React-based web framework such as Next.js, PostgreSQL, reviewed migrations, Docker, and GitHub Actions. Choose an established authentication provider instead of writing passwords and session handling from scratch. This is an architectural recommendation, not a universal “best” stack. If your team already knows Python, Django is also a sensible foundation; do not run both stacks just for this course.

Use one application with clear internal modules. A separate background worker can reuse the same codebase for email and document processing. Defer Kubernetes, microservices, vector databases and custom workflow engines until a real requirement justifies them.

text
Recruiter/candidate browser
          |
        HTTPS
          |
Web app + server endpoints
  | authentication, authorization, validation
  | business rules and audit events
  +------ PostgreSQL: structured records
  +------ Private object storage: resumes
  +------ Queue → worker: email/document processing
  +------ Identity provider: sign-in and sessions

The browser must not connect directly with privileged database credentials. An API is the server interface that receives requests, validates them, enforces access, and reads or changes data.

ATS domain model

EntityPurposeKey rule
organizationsHiring companies or business unitsBoundary for tenant isolation
users and membershipsPeople and organization rolesMembership is server-verified
jobsOpen positionsBelong to an organization
candidatesCandidate identity/contact recordsCollect only necessary information
applicationsCandidate applying to a jobSeparate from candidate identity
stage_historyApplication transitionsPreserve who changed what and when
interviewsScheduling and feedbackInterviewer access can be limited
documentsFile metadata and storage keysPrivate storage and authorized retrieval
audit_eventsSecurity/business activityNo full resumes or credentials in logs
outbox_eventsWork awaiting background deliveryCreated with the business transaction

A candidate can have more than one application. Store the hiring stage on the application, not globally on the candidate. Decide whether candidates are organization-specific; this course assumes they are. Cross-company identity sharing is a separate product and privacy decision.

Build order

  1. Fictional jobs page and candidate list.
  2. Local database and migrations.
  3. Candidate → application creation end to end.
  4. Authentication, memberships and server permissions.
  5. Stage transitions, history and audit records.
  6. Safe uploads and background processing.
  7. Interview coordination and reliable notifications.
  8. Staging deployment, operational checks and recovery rehearsal.

For the first vertical slice, define: “A recruiter in organization A creates an application for its job; it appears after reload; a user in organization B cannot access it.” That single feature exercises UI, server, database and authorization.

Lab: draw this request path and explain where organization access is checked. Success means every data-access path has a server-side boundary.

7. Docker and PostgreSQL on your Mac

Goal: run a reproducible local database without manually installing a database server.

An image is the packaged software. A container is a running instance. A volume persists database files beyond a container's lifetime. Compose describes related services in a YAML file.

For this lab, run the application on your Mac and PostgreSQL in Docker. This is intentionally smaller than containerizing the entire app immediately.

Create a local password file from the repository root:

sh
mkdir -p .secrets
chmod 700 .secrets
openssl rand -hex 24 > .secrets/postgres_password.txt
chmod 600 .secrets/postgres_password.txt

Ensure .secrets/ is ignored before running git add. Local Compose secret files remain files on your Mac; they are not a cloud secrets vault. Compose can mount secrets only into services that are granted them. See Docker Compose secrets.

Save this as compose.yaml:

yaml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: ats_local
      POSTGRES_DB: ats_dev
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ats_local -d ats_dev"]
      interval: 5s
      timeout: 3s
      retries: 10

secrets:
  postgres_password:
    file: ./.secrets/postgres_password.txt

volumes:
  postgres_data:

PostgreSQL 17 is a fixed teaching baseline. The data-directory mount above is specific to this image generation; check image instructions before upgrading major versions. Production builds should track patches and use reviewed immutable image digests.

sh
docker compose config --quiet
docker compose up -d db
docker compose ps
docker compose logs --tail=50 db
docker compose exec db psql -U ats_local -d ats_dev

In the PostgreSQL prompt:

sql
SELECT current_database(), current_user;

Type \q to exit. docker compose stop stops services; docker compose down removes containers and networks while preserving this named volume. Adding -v removes volumes and local data; do not use it unless you intend to reset the lab.

The default image initialization user is highly privileged; ats_local is only for this isolated training database. In production, the application's database role must not be a superuser, table owner or migration administrator.

Connect the application

Save a harmless .env.example:

dotenv
DATABASE_URL=postgresql://ats_local:REPLACE_WITH_LOCAL_PASSWORD@127.0.0.1:5432/ats_dev
sh
cp .env.example .env
nano .env

Use your locally generated password in .env, with your editor; keep that file out of Git and screenshots. Your chosen database library/framework must load this configuration. The generated hexadecimal password avoids connection-URL escaping issues in this lab.

When the app runs on your Mac, use 127.0.0.1; when it runs as a service in the same Compose network, use service name db. localhost inside an app container refers to that container, not the database. See Docker's PostgreSQL networking examples.

Lab: restart the database and confirm your data remains. Success means you can explain container versus volume and connect from a server process.

8. PostgreSQL, migrations and restore practice

Goal: model reliable relationships and change schemas deliberately.

In psql, try this small SQL lab. It teaches relationships and does not represent the complete ATS schema:

sql
CREATE TABLE organizations (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name text NOT NULL
);

CREATE TABLE jobs (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  organization_id bigint NOT NULL REFERENCES organizations(id),
  title text NOT NULL,
  UNIQUE (organization_id, id)
);

CREATE TABLE candidates (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  organization_id bigint NOT NULL REFERENCES organizations(id),
  email text NOT NULL,
  full_name text NOT NULL,
  UNIQUE (organization_id, email),
  UNIQUE (organization_id, id)
);

CREATE TABLE applications (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  organization_id bigint NOT NULL REFERENCES organizations(id),
  candidate_id bigint NOT NULL,
  job_id bigint NOT NULL,
  stage text NOT NULL DEFAULT 'applied'
    CHECK (stage IN ('applied','screening','interview','offer','hired','rejected','withdrawn')),
  created_at timestamptz NOT NULL DEFAULT now(),
  FOREIGN KEY (organization_id, candidate_id)
    REFERENCES candidates(organization_id, id),
  FOREIGN KEY (organization_id, job_id)
    REFERENCES jobs(organization_id, id),
  UNIQUE (organization_id, candidate_id, job_id)
);

CREATE INDEX applications_org_stage
  ON applications (organization_id, stage);

INSERT INTO organizations (name) VALUES ('Demo Hiring Company');
INSERT INTO jobs (organization_id, title)
SELECT id, 'Training Engineer' FROM organizations WHERE name = 'Demo Hiring Company';
INSERT INTO candidates (organization_id, email, full_name)
SELECT id, 'alex@example.test', 'Alex Example'
FROM organizations WHERE name = 'Demo Hiring Company';

SELECT c.full_name, j.title, a.stage
FROM applications a
JOIN candidates c ON c.id = a.candidate_id
JOIN jobs j ON j.id = a.job_id;

The final query returns no rows until you insert an application. That is intentional: a candidate record is not itself an application. Composite foreign keys stop an application from mixing organizations. The duplicate-application rule is a teaching choice; redesign it if reapplications are allowed. Decide how emails are normalized before enforcing uniqueness.

Application queries must use parameters, not concatenated user input. A library might execute SELECT ... WHERE organization_id = $1 AND id = $2 with values passed separately. The organization value comes from verified membership, not blindly from the request body.

Migrations

A migration is a versioned schema change, such as 001_initial.sql or 002_add_interview_table.sql. Adopt one migration tool suitable for your stack. Do not create tables manually in production and hope every environment matches.

Workflow: change schema → generate/write migration → read the SQL → apply to a disposable database → run integration tests → review in the pull request → run a controlled deployment migration. Once a migration is deployed, add a new migration instead of rewriting its history.

For live systems, prefer compatible changes: add a nullable column, deploy code that supports old and new shapes, backfill, then enforce the new constraint later. Rolling back an app image does not necessarily roll back its schema or data.

Back up and restore the lab

Exit psql first. These Mac commands stream a custom-format dump into a local file, then restore it to a new database:

sh
mkdir -p backups
chmod 700 backups
docker compose exec -T db pg_dump -U ats_local -d ats_dev -Fc > backups/ats_dev.dump
chmod 600 backups/ats_dev.dump
docker compose exec db createdb -U ats_local ats_restore_check
docker compose exec -T db pg_restore -U ats_local -d ats_restore_check --exit-on-error < backups/ats_dev.dump
docker compose exec db psql -U ats_local -d ats_restore_check -c 'SELECT count(*) FROM candidates;'

Use a fresh restore database name on repeated runs. Do not restore over production as a practice exercise. A dump can contain sensitive data and needs restricted access; this lab contains only synthetic records. pg_dump covers a database, not all cluster-level roles and operational recovery requirements. See PostgreSQL backup tooling.

Row-level security can add defense in depth for tenant isolation. Policies require careful database roles and transaction-scoped tenant context; superusers, bypass roles and normally table owners bypass them. Test with the actual app role, including connection-pool reuse. See PostgreSQL row security.

Lab: deliberately attempt a cross-organization relationship, expect a constraint failure, and restore your synthetic database. Success means you verify restored records rather than merely creating a backup file.

9. Build hiring workflows that survive failures

Goal: separate hiring rules from unreliable external side effects.

Example lifecycle:

text
Applied → Screening → Interview → Offer → Hired
    \          \           \         \
     └──────────┴───────────┴──────────→ Rejected
Active applications can also become Withdrawn.

Do not allow arbitrary edits to a stage string. Define allowed transitions, authorized roles and required information. Reopening a rejected application should be a separate deliberate transition with a reason, if the product permits it.

TransitionWho may request itRequired evidenceSide effect
Applied → ScreeningRecruiterValid job and applicationAssign reviewer
Screening → InterviewRecruiter/managerScreen completeScheduling task
Interview → OfferAuthorized hiring managerRequired feedback completeOffer preparation
Offer → HiredAuthorized recruiter/managerAccepted offer recordedOnboarding handoff
Active → RejectedAuthorized recruiter/managerReason and reviewOptional approved notification
Active → WithdrawnCandidate for own application or recruiterWithdrawal sourceCancel pending tasks

Reliable transition pattern

  1. Authenticate the caller and verify organization, role and application access.
  2. Validate requested transition and required information.
  3. In a database transaction, lock the application or verify its expected version.
  4. Update its stage; add history, audit entry and an outbox event.
  5. Commit the transaction.
  6. A worker delivers the outbox event to a queue or processes it reliably.
  7. Send notifications with retries, idempotency checks and failure tracking.

An idempotency key identifies a logical request so retrying it does not create another transition. Store it durably with the result and validate it against the request content. A queue often delivers more than once; consumers must tolerate duplicates. Email-provider support for idempotency varies, so document the remaining risk and reconcile delivery records.

Do not send email inside a database transaction: an external send cannot be undone by rolling back SQL. Use a dead-letter queue or failure table for work that exhausts retries. Allow operators to investigate and replay safely.

Concrete tests

AI can help draft job descriptions, summarize documents or suggest review questions. Keep hiring decisions with authorized people. Treat uploaded document text as untrusted content, not instructions to your AI tools; minimize personal data sent to providers and review inaccurate outputs.

Lab: write transition tests before adding notifications. Success means a failed external service does not lose or silently duplicate business actions.

10. Secure the ATS and its data

Goal: turn security requirements into enforceable behavior and tests.

Access model

Authentication asks who the user is. Authorization asks what that user may do. Both belong on the server. Hiding a button does not protect an endpoint.

RoleTypical access
CandidateOwn applications and approved candidate-facing documents
RecruiterAssigned or organization-permitted jobs/applications
Hiring managerAuthorized jobs, feedback and decisions
InterviewerAssigned interviews and limited candidate information
Organization administratorMembership and policy management; candidate access should be explicit

Write a permission matrix before implementation. Deny by default and check permissions on every request, including exports, file downloads and background jobs. Test guessed record IDs and users with expired or removed memberships. These principles follow OWASP authorization guidance.

Threat → control → proof

ThreatControlProof to demonstrate
Another employer sees applicantsServer organization scoping; optional tested RLSOrganization B receives denial for A's IDs and exports
Stolen sessionEstablished authentication, MFA for staff, secure session handlingLogout/revocation disables access
SQL injectionParameterized queries and validated inputMalicious input is treated as data
Unsafe resumeQuarantine, type/size checks, scanning, controlled retrievalDangerous or unscanned upload cannot be downloaded
Leaked secretRuntime secret management, secret scanning, rotationRepository and image contain no secret
Accidental data lossBackups, retention, restore drillsRestore succeeds within defined recovery targets
Excessive employee accessLeast privilege and audited exportsInterviewer cannot bulk-export candidates
Sensitive logsRedaction and restricted log accessNo tokens, resume bodies or contact details in error logs

Résumé upload workflow

Accept only supported formats and sizes. Verify extension, detected type and file structure rather than trusting a browser-provided MIME type. Generate storage keys; do not use the uploaded filename as a path. Upload into a private quarantine location, scan, then mark eligible for retrieval. Parse documents in an isolated worker with resource limits. Authorize each download before issuing a short-lived signed URL. Treat signed URLs as temporary bearer credentials and avoid logging them. These controls are based on OWASP file-upload guidance.

Practical production controls

Use HTTPS, secure HttpOnly session cookies where applicable, CSRF protections for cookie-authenticated mutations, output escaping, sensible content security policy, login and upload rate limits, and strict input validation. Use an established identity library/provider rather than inventing cryptography. Set dependency-update and vulnerability triage routines; a scanner finding needs investigation, not blind automatic changes.

For AWS, isolate the database from the public internet, restrict security groups to required connections, use least-privilege IAM roles and encrypt storage. Separate the app's limited database role from the migration role. Keep applicant files in private S3 with public access blocked. Define retention, deletion and export processes, including the treatment of retained backups. See Amazon RDS security.

Record audit events for access-sensitive changes: actor, organization, action, record identifier, timestamp and request identifier. Restrict audit access and protect events from ordinary user edits. Security audit history is different from storing every personal-data field forever.

Before using real applicants, review applicable employment, privacy, retention and AI-use requirements with your organization's responsible owners. This course's controls are engineering examples, not a compliance certification.

Lab: use two fictional organizations and all roles. Test cross-organization reads, writes, exports and downloads. Success means forbidden actions fail at the server even when requests are sent manually.

11. Development workflow and GitHub Actions

Goal: make every change traceable, reviewed and checked.

Your development workflow is separate from the application's hiring workflow:

text
GitHub issue → feature branch → AI-assisted change → local checks
→ pull request → automated checks + human review → merge
→ staging → smoke tests → approved production release → monitoring

An issue should contain the user need, acceptance criteria, permission rules and failure cases. Example: “A recruiter can move an authorized application from screening to interview; invalid transitions and other organizations' applications are denied; successful changes create history and an outbox event.”

CI template

The following .github/workflows/ci.yml is a minimal template. It works only after the named scripts and lockfile exist. Define meaningful tests; do not add scripts that return success without checking behavior.

yaml
name: CI
on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  checks:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build

The action tags are readable teaching examples. For hardened workflows, verify supported releases and pin third-party actions to reviewed full commit identifiers, with an update process. npm ci uses the committed lockfile. See GitHub's Node CI tutorial.

Extend CI with an isolated PostgreSQL service for integration tests and a disposable application environment for browser tests. Run migrations on the test database before testing. Never point PR test jobs at the production database or supply production secrets to untrusted contributions.

GitHub interface settings

In Settings, configure a ruleset or available branch protections for main: require pull requests, relevant checks and review; restrict direct changes. Keep permissions narrow. In Actions, click a failing job and inspect its step output; redact sensitive data before sharing it with AI. Use Issues/Projects to track fixes rather than hiding failures by disabling checks.

For deployments from GitHub to AWS, prefer OIDC for short-lived credentials. Restrict AWS trust to the intended repository and branch or protected environment, and grant only the deployment permissions required. Do not store permanent administrator AWS keys in GitHub. See GitHub OIDC for AWS.

Lab: deliberately break a meaningful test on a branch. Confirm CI fails and merging is blocked under your configured rules. Fix it and observe green checks.

12. Interface with AWS and prepare a release

Goal: understand how your local package becomes an operated service.

For this training architecture, use ECS Fargate for the app container, RDS PostgreSQL for the managed database, ECR for images, S3 for private documents, Secrets Manager for secrets, CloudWatch for logs/alarms, and SQS for background work. This is a proposed AWS design; a small prototype can begin locally and add cloud services gradually.

ServicePlain-language purpose
ECRStore versioned container images
ECS/FargateRun containers without managing individual servers
Application Load Balancer + ACMRoute web requests and provide HTTPS certificates
RDS PostgreSQLManaged relational database
S3Private document storage
SQSBuffer background jobs and retries
Secrets ManagerStore runtime credentials with controlled access
IAMDefine which people and services can act
CloudWatchLogs, metrics, alarms and operational visibility
VPC/security groupsNetwork placement and permitted connections

Recommended network shape

text
Internet → HTTPS load balancer in public subnets
                    ↓
             App tasks in private subnets
              ↓           ↓          ↓
         Private RDS   Private S3   SQS/worker

Load balancer rules admit HTTPS; app security group permits only the load balancer's app traffic; database security group permits only required app/worker database traffic. Private tasks need a deliberate path to required services and image pulls through suitable VPC endpoints or outbound networking. NAT gateways, endpoints and load balancers can create ongoing charges; design and budget them explicitly.

Connect from your Mac

If your organization uses IAM Identity Center, configure a development profile with its supplied start URL, region and role. See AWS CLI SSO setup.

sh
aws configure sso --profile ats-dev
aws sso login --profile ats-dev
aws sts get-caller-identity --profile ats-dev
aws configure get region --profile ats-dev

Check account ID and role before making changes. Do not use the root account for development. If Identity Center is not configured, have the account owner establish a suitable access method rather than copying root access keys into Terminal.

Build and push an image: conditional lab

The commands below require a reviewed Dockerfile, authenticated development profile, an existing ECR repository named ats-training, and permission to push. They do not create ECS, RDS or a secure deployment by themselves. Replace the region with your actual configured region.

sh
export AWS_PROFILE=ats-dev
export AWS_REGION=us-east-1
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
IMAGE_TAG=$(git rev-parse --short=12 HEAD)

aws ecr get-login-password --region "$AWS_REGION" |
  docker login --username AWS --password-stdin "$ECR_REGISTRY"

docker buildx build --platform linux/amd64 \
  -t "$ECR_REGISTRY/ats-training:$IMAGE_TAG" --push .

This example chooses x86_64 deployment; set the ECS task runtime architecture to match. Apple Silicon can build a different architecture, so be explicit. Do not reuse a commit tag for different builds; promote the exact tested image digest. The app image should run as a non-root user, contain only needed runtime files, and receive secrets at runtime. ECR authentication and tagging are described in AWS's image-push guide.

Deployment workflow

  1. Provision reviewed infrastructure in a separate development/staging environment. Keep infrastructure state protected and out of ordinary Git files when it contains secrets.
  2. Build and scan the image; push it to ECR and record its digest.
  3. Configure an ECS task definition: image, CPU/memory, architecture, health checks, logs, task role and execution role.
  4. Use Secrets Manager references for database/auth credentials. Grant the execution role access only to required secrets; give the task role only the application's needed AWS actions. Updating an injected secret requires new tasks to load it. See ECS secret handling.
  5. Run compatible schema migrations once through a controlled job with a migration role and private database access. Avoid every app task trying to migrate at startup.
  6. Deploy staging; test login, application creation, role denials, uploads and retries.
  7. Approve production, deploy the same artifact, and monitor health/error rates.
  8. If needed, revert to the previous compatible task definition. Handle schema/data recovery separately according to the runbook.

Use infrastructure as code such as AWS CDK, CloudFormation or Terraform once the concepts are clear. Choose one; preserve reviewable changes and reproducibility. The initial lab can use the AWS console to learn resource relationships, but document every setting before relying on the environment.

Operational readiness

Choose service targets instead of assuming “cloud” means reliable. Example training targets: recover no more than 24 hours of data and restore service within four hours. These are proposed recovery point/time objectives, not guarantees; test whether your backups and staffing can meet them. Production needs may be stricter.

Monitor request errors, latency, database connections, queue age, failed jobs, upload-scan failures and unexpected access denials. Redact logs. Establish alert ownership, backups/point-in-time recovery, restore rehearsals, dependency patching and a deployment rollback procedure. A multi-zone database improves availability but does not replace backups.

Set AWS budget alerts before deploying. Budget alerts are notifications, not a universal hard spending cap. Tear down disposable lab resources through your infrastructure tool, verify remaining databases, snapshots, object storage and networking resources, and preserve only what you intend to retain.

Lab: explain which account/profile you are using, where the database is reachable, how tasks receive secrets, and how you would restore a failed release. Success means you can walk through the plan without granting public database access.

Capstone: demonstrate a reviewable ATS slice

Build a fictional two-organization ATS with a job, candidate, application and stage change. It does not need every enterprise feature to demonstrate solid engineering.

Acceptance checklist

If an item is incomplete, record it openly as a limitation. A attractive interface alone is not a strong technical package; the package needs demonstrable access boundaries, reliable state changes, reproducible builds and recovery.

Review questions with answer cues

  1. Why did my saved file not appear on GitHub? Saving, staging, committing and pushing are separate steps.
  2. Why does my app container fail to connect using localhost? It must address the database service on the container network.
  3. Why can a candidate have multiple hiring stages? Stages belong to applications, which relate candidates to jobs.
  4. Why isn't a hidden button sufficient security? Direct server requests remain possible.
  5. What if email fails after a stage change? Durable outbox/queue work retries without losing the transition.
  6. What proves my backup works? A verified restore with tested recovery time.
  7. What does a green CI badge fail to prove? Behaviors absent from meaningful tests, cloud configuration and production operations.
  8. Why use separate app and migration database roles? Runtime code should not have schema-owner or administrator privileges.
  9. What if a secret is committed? Revoke/rotate it and review exposure; deleting the file is insufficient.
  10. What makes this technically strong? Clear scope, maintained dependencies, enforceable rules, tested permissions, reliable delivery and documented operations.

Terminal troubleshooting card

SymptomFirst checkNext step
command not foundwhich COMMAND and installation instructionsCorrect the PATH; open a fresh Terminal
not a git repositorypwd and ls -laEnter the folder containing .git
Commit asks for identitygit config user.name / git config user.emailConfigure your identity
Push fails authenticationgh auth statusSign in and run gh auth setup-git
Push rejectedgit fetch origin, inspect historyReconcile remote changes; avoid reflexive force-push
Docker daemon unavailableIs Docker Desktop running?Start it and wait for readiness
Database port in uselsof -i :5432Stop the intended conflicting service or choose a different host port
DB password change has no effectExisting initialized volume?Initialization settings don't update existing roles; change password deliberately
Migration failsRead first database errorCheck schema state and migration ordering
AWS access deniedCaller identity and profileReview role permissions; do not grant admin as a shortcut
Image runs locally but not on ECSPlatform/CPU architecture and startup logsMatch architecture and runtime settings

Your repeatable daily routine

Read the issue and acceptance criteria. Update main and create a branch. Ask AI for a bounded change. Inspect the diff, run relevant checks and test the actual behavior. Commit, push and open a pull request. Read review feedback and CI results. Merge and deploy through the documented workflow. Observe the result and close the issue only when its acceptance criteria are met.

Appendix A: Git commands in one place

Here is a practical reference for Mac Terminal. Run commands inside your project folder unless you are creating or cloning a repository. Replace placeholders such as REPOSITORY_URL, COMMIT_ID and filename with your actual values. Do not type a terminal prompt such as % or $ before these commands.

Create and configure

CommandWhat it does
git --versionCheck your installed Git version
git config --global user.name "Your Name"Set the name attached to your commits
git config --global user.email "you@example.com"Set your commit email; use your GitHub-approved or no-reply email
git init -b mainTurn the current folder into a Git repository
git clone REPOSITORY_URLDownload a repository into a new folder

Review and save changes

CommandWhat it does
git statusShow your branch and changed files
git diffShow changes you have not staged
git add filenameStage a specific file
git add .Stage all changes in the current folder and its subfolders
git diff --stagedReview what your next commit will contain
git commit -m "Describe the change"Save staged changes as a local checkpoint
git log --oneline -10Show the last 10 commits
git show COMMIT_IDInspect a specific commit

Review before committing, especially when using git add ., to avoid including passwords or unwanted files. Ignore rules do not stop changes to files already tracked by Git.

Work with branches

CommandWhat it does
git branchList local branches
git branch -aList local and remote-tracking branches
git switch -c feat/candidate-listCreate and switch to a new branch
git switch mainSwitch to the main branch
git merge feat/candidate-listMerge that branch into your current branch
git branch -d feat/candidate-listDelete a local branch when Git's merge-safety checks permit it

For shared projects, normally merge through a reviewed GitHub pull request. Deleting a local branch does not delete its remote branch. A squash-merged branch can still fail Git's local merge-safety check; inspect the history before forcing deletion.

Connect and synchronize with GitHub

CommandWhat it does
git remote -vShow remote repository addresses
git remote add origin REPOSITORY_URLConnect your local repository to a remote named origin
git fetch originDownload remote updates without merging
git pull --ff-onlyUpdate your branch if Git can do so without merging divergent histories
git push -u origin mainPush main and remember its remote connection
git push -u origin feat/candidate-listPush a new feature branch and remember its remote connection
git pushPush commits to your configured remote branch under the usual setup

Use git remote add origin only when that remote does not already exist. The -u option sets an upstream connection so later pushes can use the shorter command. If a pull or push is rejected, inspect the branch history before deciding how to reconcile changes.

Temporarily save or undo work

CommandWhat it does
git stash push -m "Work in progress"Temporarily store tracked-file changes
git stash push -u -m "Work in progress"Also include untracked files; ignored files remain excluded
git stash listList saved stashes
git stash popRestore the latest stash; conflicts may need resolving
git restore --staged filenameUnstage a file while keeping your edits
git restore filenameDiscard unstaged edits to that tracked file, restoring its staged version
git revert COMMIT_IDCreate a new commit that reverses an earlier commit; conflicts may need resolving
git merge --abortCancel a merge in progress

Be careful: git restore filename discards unstaged edits. Commands such as git reset --hard, git clean -fd and git push --force can discard work or overwrite shared history. A stash is temporary local storage, not an off-device backup. Start a merge with a clean working tree so aborting it can restore your starting state reliably.

Your typical daily sequence

Start with a clean working tree, or deliberately preserve unfinished work first. Run this from your repository:

sh
git switch main
git pull --ff-only
git switch -c feat/candidate-list

# Make your changes and run relevant checks.

git status
git diff
git add src/server/candidates.ts
git diff --staged
git commit -m "feat: add candidate list"
git push -u origin feat/candidate-list

Use a real changed file in place of the example path. Then open GitHub and create a pull request to review and merge the branch. After merging, return to main and update it:

sh
git switch main
git pull --ff-only

Remember: add prepares changes, commit saves them locally, and push sends commits to GitHub.