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
| Lesson | What you learn | Evidence you can do it |
|---|---|---|
| 1 | Mac terminal and tool setup | Find your project and check installed tools |
| 2 | GitHub interface and a new repository | Create a private repository and push a README |
| 3 | Repository contents | Organize code, configuration, documentation and tests |
| 4 | Everyday Git and pull requests | Make, review and merge a small feature |
| 5 | Coding with a terminal AI assistant | Give a bounded task and verify its changes |
| 6 | ATS architecture and first feature | Trace browser → server → database |
| 7 | Docker and local PostgreSQL | Start a database and connect to it |
| 8 | SQL, migrations and recovery | Create related tables and restore a backup |
| 9 | Applicant and hiring workflows | Define transitions, retries and audit events |
| 10 | Security and applicant privacy | Demonstrate access denial and safe uploads |
| 11 | GitHub Actions and delivery | Reject a broken change before deployment |
| 12 | AWS and operational readiness | Explain 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.
| Command | Meaning |
|---|---|
pwd | Show your current folder |
ls -la | List files, including hidden files |
cd ~/Developer | Move into a folder |
cd .. | Move up one folder |
mkdir -p ~/Developer/ats-training | Create folders |
touch README.md | Create an empty file if missing |
cat README.md | Print a small text file |
open . | Open the current folder in Finder |
nano README.md | Edit a text file in Terminal |
which git | Locate 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:
mkdir -p ~/Developer/ats-training
cd ~/Developer/ats-training
pwdInstall Apple's command-line tools if Git is unavailable:
xcode-select --install
git --versionInstall 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:
brew install git gh node@24 awscliFollow 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.
git --version
gh --version
node --version
npm --version
docker version
docker compose version
aws --versionLab: 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
| Shortcut | Action |
|---|---|
| Command–Space | Spotlight: launch an app or find a file |
| Command–Tab | Switch applications |
| Command–backtick | Switch windows in the current app |
| Command–C / Command–V | Copy / paste |
| Command–Z | Undo |
| Command–S | Save |
| Command–F | Find text |
| Command–W / Command–Q | Close window / quit app |
| Command–comma | Current app settings |
| Control–Command–Q | Lock screen |
| Shift–Command–G in Finder | Jump to a typed folder path |
| Command–Up Arrow in Finder | Parent folder |
| Space in Finder | Quick Look preview |
| Shift–Command–period in Finder | Toggle hidden files |
| Shift–Command–4 | Capture selected screen area |
| Shift–Command–5 | Screenshot/recording controls |
Shortcuts can vary with application or configured keyboard settings. See Apple's shortcut reference and screenshot guide.
Faster Terminal use
| Key | Action |
|---|---|
| Tab | Complete a path or command |
| Up / Down Arrow | Recall command history |
| Control–A / Control–E | Move to start / end of shell command line |
| Control–U | Remove text before cursor in the usual zsh editing mode |
| Control–K | Remove text after cursor |
| Control–R | Search command history in the usual shell configuration |
| Control–C | Interrupt the running foreground command |
| Command–T | New Terminal tab |
| Command–plus / Command–minus | Adjust 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
# 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 openSpotlight 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:
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 area | What you do there | ATS example |
|---|---|---|
| Code | Browse files, select branches, copy clone URL | Read the API and README |
| Issues | Describe work, defects and acceptance criteria | “Recruiter can move application to interview” |
| Pull requests | Review a branch before merging | Check authorization and tests |
| Actions | See automated build and test results | Find why a test failed |
| Projects | Organize issues into a work board | Backlog → Ready → Doing → Review → Done |
| Security | Review available security features and findings | Dependency alerts and exposed secrets |
| Settings | Manage access, rules, environments and configuration | Require 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
- Sign in to GitHub and enable account MFA.
- Use the plus menu → New repository.
- Choose the owner and name
ats-training; select Private. - 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. - Create the repository. Select Code → HTTPS and copy its URL.
- Authenticate and clone below. Replace
YOUR-USERNAMEbefore running.
gh auth login
gh auth setup-git
cd ~/Developer
git clone https://github.com/YOUR-USERNAME/ats-training.git
cd ats-training
git statusIf 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.
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 --webIf 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:
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| Include | Why |
|---|---|
| README | Purpose, prerequisites, install/start/test commands, configuration and troubleshooting |
| Dependency manifest and lockfile | Record dependencies and reproducible resolved versions |
.env.example | Show configuration names and harmless placeholders |
| Migrations | Review and reproduce database changes |
| Synthetic seed data | Demonstrate features without personal data |
| Tests | Protect behavior, permission boundaries and workflows |
| Infrastructure definitions | Review and recreate cloud configuration |
| Architecture decisions | Explain why a choice was made and its tradeoffs |
| Runbook | Describe 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:
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:
mkdir -p src/web src/server db/migrations db/seed
mkdir -p tests/unit tests/integration tests/e2e docs/decisions infra .github/workflowsGit 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.
| Command | What it does |
|---|---|
git status | Show branch and changed/staged files |
git diff | Show changes not yet staged |
git add README.md | Stage one file for the next commit |
git diff --staged | Review exactly what the commit will contain |
git commit -m "message" | Save the staged checkpoint locally |
git push | Send commits to the configured remote branch |
git fetch origin | Download remote history without merging it |
git pull --ff-only | Update when a simple forward update is possible |
git switch -c feat/candidate-list | Create and enter a feature branch |
git log --oneline -8 | Inspect recent checkpoints |
git remote -v | Show remote repository addresses |
A complete feature flow, after the application and scripts exist:
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:
git restore --staged README.mdTo preserve unfinished work while switching tasks:
git stash push -m "unfinished candidate list"
git stash list
git stash popTo 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.
cd ~/Developer/ats-training
git status
git switch -c feat/first-ai-task
codexA 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:
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
- Ask it to explain the plan and the files it will change.
- Let it implement a bounded task on a branch.
- Inspect
git diff; look for new dependencies, credentials, removed tests, relaxed permissions and unrelated edits. - Run tests yourself and exercise the feature in the browser.
- Ask a follow-up review question: “What could allow another organization to see this data?”
- 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.
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 sessionsThe 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
| Entity | Purpose | Key rule |
|---|---|---|
| organizations | Hiring companies or business units | Boundary for tenant isolation |
| users and memberships | People and organization roles | Membership is server-verified |
| jobs | Open positions | Belong to an organization |
| candidates | Candidate identity/contact records | Collect only necessary information |
| applications | Candidate applying to a job | Separate from candidate identity |
| stage_history | Application transitions | Preserve who changed what and when |
| interviews | Scheduling and feedback | Interviewer access can be limited |
| documents | File metadata and storage keys | Private storage and authorized retrieval |
| audit_events | Security/business activity | No full resumes or credentials in logs |
| outbox_events | Work awaiting background delivery | Created 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
- Fictional jobs page and candidate list.
- Local database and migrations.
- Candidate → application creation end to end.
- Authentication, memberships and server permissions.
- Stage transitions, history and audit records.
- Safe uploads and background processing.
- Interview coordination and reliable notifications.
- 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:
mkdir -p .secrets
chmod 700 .secrets
openssl rand -hex 24 > .secrets/postgres_password.txt
chmod 600 .secrets/postgres_password.txtEnsure .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:
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.
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_devIn the PostgreSQL prompt:
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:
DATABASE_URL=postgresql://ats_local:REPLACE_WITH_LOCAL_PASSWORD@127.0.0.1:5432/ats_devcp .env.example .env
nano .envUse 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:
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:
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:
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.
| Transition | Who may request it | Required evidence | Side effect |
|---|---|---|---|
| Applied → Screening | Recruiter | Valid job and application | Assign reviewer |
| Screening → Interview | Recruiter/manager | Screen complete | Scheduling task |
| Interview → Offer | Authorized hiring manager | Required feedback complete | Offer preparation |
| Offer → Hired | Authorized recruiter/manager | Accepted offer recorded | Onboarding handoff |
| Active → Rejected | Authorized recruiter/manager | Reason and review | Optional approved notification |
| Active → Withdrawn | Candidate for own application or recruiter | Withdrawal source | Cancel pending tasks |
Reliable transition pattern
- Authenticate the caller and verify organization, role and application access.
- Validate requested transition and required information.
- In a database transaction, lock the application or verify its expected version.
- Update its stage; add history, audit entry and an outbox event.
- Commit the transaction.
- A worker delivers the outbox event to a queue or processes it reliably.
- 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
- Two recruiters move the same application at once: only a valid single transition commits.
- A notification provider is unavailable: the stage remains recorded and delivery retries later.
- A worker receives an event twice: duplicate work is detected.
- An interviewer tries to create an offer: access is denied.
- An application is withdrawn: pending reminders stop.
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.
| Role | Typical access |
|---|---|
| Candidate | Own applications and approved candidate-facing documents |
| Recruiter | Assigned or organization-permitted jobs/applications |
| Hiring manager | Authorized jobs, feedback and decisions |
| Interviewer | Assigned interviews and limited candidate information |
| Organization administrator | Membership 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
| Threat | Control | Proof to demonstrate |
|---|---|---|
| Another employer sees applicants | Server organization scoping; optional tested RLS | Organization B receives denial for A's IDs and exports |
| Stolen session | Established authentication, MFA for staff, secure session handling | Logout/revocation disables access |
| SQL injection | Parameterized queries and validated input | Malicious input is treated as data |
| Unsafe resume | Quarantine, type/size checks, scanning, controlled retrieval | Dangerous or unscanned upload cannot be downloaded |
| Leaked secret | Runtime secret management, secret scanning, rotation | Repository and image contain no secret |
| Accidental data loss | Backups, retention, restore drills | Restore succeeds within defined recovery targets |
| Excessive employee access | Least privilege and audited exports | Interviewer cannot bulk-export candidates |
| Sensitive logs | Redaction and restricted log access | No 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:
GitHub issue → feature branch → AI-assisted change → local checks
→ pull request → automated checks + human review → merge
→ staging → smoke tests → approved production release → monitoringAn 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.
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 buildThe 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.
| Service | Plain-language purpose |
|---|---|
| ECR | Store versioned container images |
| ECS/Fargate | Run containers without managing individual servers |
| Application Load Balancer + ACM | Route web requests and provide HTTPS certificates |
| RDS PostgreSQL | Managed relational database |
| S3 | Private document storage |
| SQS | Buffer background jobs and retries |
| Secrets Manager | Store runtime credentials with controlled access |
| IAM | Define which people and services can act |
| CloudWatch | Logs, metrics, alarms and operational visibility |
| VPC/security groups | Network placement and permitted connections |
Recommended network shape
Internet → HTTPS load balancer in public subnets
↓
App tasks in private subnets
↓ ↓ ↓
Private RDS Private S3 SQS/workerLoad 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.
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-devCheck 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.
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
- Provision reviewed infrastructure in a separate development/staging environment. Keep infrastructure state protected and out of ordinary Git files when it contains secrets.
- Build and scan the image; push it to ECR and record its digest.
- Configure an ECS task definition: image, CPU/memory, architecture, health checks, logs, task role and execution role.
- 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.
- 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.
- Deploy staging; test login, application creation, role denials, uploads and retries.
- Approve production, deploy the same artifact, and monitor health/error rates.
- 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
- Why did my saved file not appear on GitHub? Saving, staging, committing and pushing are separate steps.
- Why does my app container fail to connect using localhost? It must address the database service on the container network.
- Why can a candidate have multiple hiring stages? Stages belong to applications, which relate candidates to jobs.
- Why isn't a hidden button sufficient security? Direct server requests remain possible.
- What if email fails after a stage change? Durable outbox/queue work retries without losing the transition.
- What proves my backup works? A verified restore with tested recovery time.
- What does a green CI badge fail to prove? Behaviors absent from meaningful tests, cloud configuration and production operations.
- Why use separate app and migration database roles? Runtime code should not have schema-owner or administrator privileges.
- What if a secret is committed? Revoke/rotate it and review exposure; deleting the file is insufficient.
- What makes this technically strong? Clear scope, maintained dependencies, enforceable rules, tested permissions, reliable delivery and documented operations.
Terminal troubleshooting card
| Symptom | First check | Next step |
|---|---|---|
command not found | which COMMAND and installation instructions | Correct the PATH; open a fresh Terminal |
not a git repository | pwd and ls -la | Enter the folder containing .git |
| Commit asks for identity | git config user.name / git config user.email | Configure your identity |
| Push fails authentication | gh auth status | Sign in and run gh auth setup-git |
| Push rejected | git fetch origin, inspect history | Reconcile remote changes; avoid reflexive force-push |
| Docker daemon unavailable | Is Docker Desktop running? | Start it and wait for readiness |
| Database port in use | lsof -i :5432 | Stop the intended conflicting service or choose a different host port |
| DB password change has no effect | Existing initialized volume? | Initialization settings don't update existing roles; change password deliberately |
| Migration fails | Read first database error | Check schema state and migration ordering |
| AWS access denied | Caller identity and profile | Review role permissions; do not grant admin as a shortcut |
| Image runs locally but not on ECS | Platform/CPU architecture and startup logs | Match 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
| Command | What it does |
|---|---|
git --version | Check 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 main | Turn the current folder into a Git repository |
git clone REPOSITORY_URL | Download a repository into a new folder |
Review and save changes
| Command | What it does |
|---|---|
git status | Show your branch and changed files |
git diff | Show changes you have not staged |
git add filename | Stage a specific file |
git add . | Stage all changes in the current folder and its subfolders |
git diff --staged | Review what your next commit will contain |
git commit -m "Describe the change" | Save staged changes as a local checkpoint |
git log --oneline -10 | Show the last 10 commits |
git show COMMIT_ID | Inspect 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
| Command | What it does |
|---|---|
git branch | List local branches |
git branch -a | List local and remote-tracking branches |
git switch -c feat/candidate-list | Create and switch to a new branch |
git switch main | Switch to the main branch |
git merge feat/candidate-list | Merge that branch into your current branch |
git branch -d feat/candidate-list | Delete 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
| Command | What it does |
|---|---|
git remote -v | Show remote repository addresses |
git remote add origin REPOSITORY_URL | Connect your local repository to a remote named origin |
git fetch origin | Download remote updates without merging |
git pull --ff-only | Update your branch if Git can do so without merging divergent histories |
git push -u origin main | Push main and remember its remote connection |
git push -u origin feat/candidate-list | Push a new feature branch and remember its remote connection |
git push | Push 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
| Command | What 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 list | List saved stashes |
git stash pop | Restore the latest stash; conflicts may need resolving |
git restore --staged filename | Unstage a file while keeping your edits |
git restore filename | Discard unstaged edits to that tracked file, restoring its staged version |
git revert COMMIT_ID | Create a new commit that reverses an earlier commit; conflicts may need resolving |
git merge --abort | Cancel 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:
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-listUse 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:
git switch main
git pull --ff-onlyRemember: add prepares changes, commit saves them locally, and push sends commits to GitHub.