Welcome on board 🚀
I’m cutting you the straight‑to‑the‑point guide I wish I’d had on day one. In the next couple of hours you’ll:
- Bootstrap a secure, repeatable dev machine.
- Ship a “hello‑world” Rust CLI locally.
- Open your first pull request in Stratorys codebase.
Bookmark this book, keep a terminal open, and treat every code block as something you actually type. If a step fails, open an issue—docs are living code.
Pre‑flight checklist
| ✔ | Item |
|---|---|
| macOS / Linux | |
Admin rights (or someone with sudo) | |
| GitHub account with 2‑factor enabled | |
| Patience for ~3 GB of toolchain downloads |
How this book is organised
- Setup → Security → Git get you ready to clone and build.
- Rust → Exercises learn and understand the language + practice.
- Networking → Data Structures other importants knwoledge.
- Contributing walks you through a tiny feature + PR so you can merge.
Need help?
Ask in #onboarding Slack or ping your mentor. Stuck on a command? Post the exact error and the last step you ran—we debug fast when the signal is high.
Happy hacking ✌️
Operating System Setup
I assume a fresh machine; feel free to skip steps you’ve nailed already.
Common ground (macOS & Linux)
1. Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Homebrew manages everything from git to postgresql.
Add Brew to PATH (the installer prints the snippet) and run brew doctor until you’re "ready to brew".
2. Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup installs stable Rust plus cargo, the build system. Leave default targets.
3. Oh My Zsh (optional but nice)
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
Gives you a battery‑included zsh config, plugins, and a pretty prompt.
Enable the git and rust plugins in ~/.zshrc for sweet autocompletions.
macOS notes
- Install Xcode Command‑Line Tools:
xcode-select --install
Linux (Ubuntu)
sudo apt update && sudo apt install build-essential curl file git
sudo apt install zsh docker.io
chsh -s $(which zsh)
sudo usermod -aG docker $USER
Log out/in to refresh group membership.
Verify setup
rustc --version
cargo new sanity-check && cd sanity-check && cargo run
git --version
docker --version
Editors & IDEs
Pick one primary editor, recommand Zed or VSCode
Visual Studio Code (recommended)
- Install VS Code from the official site. :contentReference[oaicite:0]{index=0}
- Launch and hit
⌘⇧P-> Extensions: Install -> search rust‑analyzer.
rust‑analyzer gives near‑instant completions, inline type hints and “run test” - Add these Rust components:
rustup component add clippy rustfmt
clippy lints common foot‑guns; rustfmt enforces style on save.
- Nice‑to‑have extensions:
| Extension | Why |
|---|---|
| Error Lens | Highlights compiler errors inline. |
Security
Security is a critical aspect of our development workflow. This section covers essential security practices that every team member must implement.
Best Practices for Security
- Strong Passwords: Always use strong, unique passwords for all your accounts. A strong password should be at least 16 characters long, combining letters, numbers, and special characters. Avoid using easily guessable information, such as birthdays or common words.
- Two-Factor Authentication (2FA): Enable two-factor authentication on all accounts that offer it. 2FA adds an extra layer of security by requiring not only a password and username but also something that only the user has on them, i.e., a piece of information only they should know or have immediately to hand - such as a physical token.
- Lock Your Laptop: Make it a habit to lock your laptop whenever you step away from it, even if it's just for a short period. This practice prevents unauthorized access to company data.
- Awareness in Public Spaces: When working remotely, especially in public spaces, be mindful of your surroundings. Ensure that no one can overlook your screen. This caution helps prevent shoulder surfing, where an attacker can obtain confidential information by watching your screen.
- Encrypt Your Hard Drive: Encrypt the hard drive of your laptop and any other devices you use for work. Encryption converts the data stored on your device into unreadable code that cannot be easily deciphered by unauthorized people.
- SSH Key: Use SSH keys for secure access to your repositories. SSH keys are a pair of cryptographic keys that can be used to authenticate your identity without the need for a password.
These practices are non-negotiable for working with our repositories and systems.
SSH Keys
SSH keys provide secure authentication for Git operations and server access. We standardize on ED25519 keys for their security and performance advantages.
Generating ED25519 SSH Keys
- Open Terminal and run:
ssh-keygen -t ed25519 -C "your_email@example.com"
-
When prompted for a file location, press Enter to use the default location
-
Enter a secure passphrase (required for our team)
-
Verify your key was created:
ls -la ~/.ssh
You should see:
id_ed25519(private key - never share this)id_ed25519.pub(public key - safe to share)
Adding Your SSH Key to GitHub
- Copy your public key to clipboard:
pbcopy < ~/.ssh/id_ed25519.pub # macOS
# OR
cat ~/.ssh/id_ed25519.pub # Linux (copy manually)
-
Go to GitHub -> Settings -> SSH and GPG keys -> New SSH key
-
Give your key a descriptive title (e.g., "Work MacBook Pro")
-
Paste your public key in the "Key" field
-
Click "Add SSH key"
-
Test your connection:
ssh -T git@github.com
You should see: "Hi username! You've successfully authenticated..."
Key Management Best Practices
- Use a password manager to store your SSH key passphrase
- Never share your private key
- Use different keys for different services when possible
- Consider using
ssh-agentto avoid typing your passphrase repeatedly:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Git & GitHub
This chapter is the muscle‑memory playbook: minimal theory, maximal commands.
By the end you can clone, branch, commit, rebase, and open a pull‑request that sails through CI.
1. First‑time config
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase true # rebase by default
git config --global color.ui auto
user.name and user.email stamp every commit; GitHub shows them in blame views.
The defaults above keep your history linear and readable.
2. Cloning a repo
git clone git@github.com:stratorys/project.git
cd project
git clone creates a working directory, a .git folder, and a remote named origin pointing at GitHub.
3. Branching workflow
git checkout -b feat/login‑oauth # create & switch
git checkout main # switch to an existing branch
Branches are cheap pointers; create one per task, push early, delete when merged. Upstream rules:
- prefix
feat/,fix/,chore/,hotfix/ - Keep them rebased on
mainto avoid diamond graphs.
4. Commit hygiene
Follow Conventional Commits:
feat(auth): add OAuth2 PKCE flow
BODY: rationale, links, screenshots if UI.
BREAKING CHANGE: drops legacy /v1 token endpoint
The spec enables automatic changelogs and semantic version bumps. Aim for focused commits; if your diff is >300 lines, split it.
5. Syncing with main
git checkout main
git pull origin main
git checkout - # `-` is shorthand for previous branch
git rebase main
rebase rewrites your branch atop the latest main so the merge commit stays clean.
6. Remotes & pushing
git remote add origin git@github.com:stratorys/project.git
git push -u origin feat/login‑oauth
-u sets up upstream tracking so future git push / git pull need no extra flags.
7. Opening a pull request
On GitHub: Choose your branch → New pull request → fill template → create.
CLI alternative:
gh pr create --fill
gh pr checkout 123 # review someone else’s PR
gh pr checkout grabs the branch locally for testing. ([GitHub CLI][11])
Quick‑ref commands
git status
git add . # stage all changes
git commit -m "fix: add OAuth2 PKCE flow"
git commit -m "fix: add OAuth2 PKCE flow" --amend # amend last commit
git log --oneline --graph --decorate # view commit history
git diff # view unstaged changes
git diff --cached # view staged changes
git diff HEAD # view all changes since last commit
git reset HEAD~1 # undo last commit (keep changes)
git pull origin main # pull changes from remote
git push origin main # push changes to remote
Databases · PostgreSQL
We run PostgreSQL because it’s ACID‑safe, battle‑tested, and pairs cleanly with Rust ORMs like sqlx and Diesel. Everything here targets Postgres 16 but 95 % works back to 12.
1 · Spin‑up
docker run --rm -d --name postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=mydb -p 5432:5432 postgres
2 · Connect with psql
psql postgres://postgres:postgres@localhost:5432/postgres
Quick meta‑commands:
| Command | What it does |
|---|---|
\l | list databases |
\c db | connect / switch |
\dt | list tables |
\d tbl | describe table/columns |
\q | quit |
All meta‑commands are baked in psql.
3 · Transactions 101
BEGIN;
UPDATE account SET balance = balance - 10 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;
COMMIT; -- or ROLLBACK;
One bad statement ? Roll back and your data stays consistent-as per the SQL standard Postgres implements.
4 · Performance quick wins
- Indexes-create them on columns used in
WHEREor join predicates:
CREATE INDEX idx_user_email ON users (email);
Index syntax and caveats live in the official docs.
- Explain plans-confirm the planner hits that index:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'a@b.com';
Green numbers = faster path, red = full table scan.
5 · UUIDs in Postgres
Postgres ships a native uuid type; we prefer UUIDv7 (time‑ordered) for better index locality in event stores. Use the pgcrypto extension until official uuidv7() ships.
6 · Migrations
We version schema in Git with sqlx migrate (see /contributing/cli_tool.md). One migration = one file pair:
20250511103000_create_users.up.sql
20250511103000_create_users.down.sql
Never edit a committed migration; write a new one.
Next stop → SQL Basics.
SQL Basics
The goal: read and write 80 % of queries you’ll ever need.
1 · SELECT anatomy
SELECT column_list
FROM table
WHERE predicate
GROUP BY cols
HAVING aggregate_predicate
ORDER BY cols
LIMIT n OFFSET m;
Only SELECT … FROM … is mandatory; everything else is optional and ordered exactly like above per spec.
1.1 · Filtering (WHERE)
SELECT * FROM orders
WHERE status = 'shipped'
AND created_at >= NOW() - INTERVAL '30 days';
WHERE happens before grouping—filter early for speed.
1.2 · Aggregation (GROUP BY, HAVING)
SELECT country, COUNT(*) AS users
FROM accounts
GROUP BY country
HAVING COUNT(*) > 100;
HAVING filters after aggregation; ideal for “top N” style analytics.
2 · Writing Data
INSERT INTO users (id, email) VALUES (gen_random_uuid(), 'a@b.com');
UPDATE users SET last_login = NOW() WHERE id = '…';
DELETE FROM users WHERE last_login < NOW() - INTERVAL '1 year';
Wrap multiple writes in a transaction to keep data consistent.
3 · Data types you’ll meet daily
| Type | Example | Notes |
|---|---|---|
INTEGER | 42 | 32‑bit signed |
BIGINT | 9007199254740991 | 64‑bit |
TEXT | 'hello' | un‑bounded string |
BOOLEAN | TRUE | 1‑byte |
UUID | '550e8400‑e29b‑41d4‑a716‑446655440000' | native support, RFC 4122 compliant |
JSONB | '{"k":"v"}' | binary JSON with GIN indexes |
Next stop → Joins.
Joins
Tables rarely live alone; joins stitch them. Master these four and you’re set.
1 · INNER JOIN (the default)
SELECT u.id, o.id, o.total
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;
Returns rows where the join predicate matches in both tables.
2 · LEFT JOIN (keep left‑hand rows)
SELECT u.id, o.id, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
Users with zero orders still appear; o.* is NULL. Handy for “show latest activity or None”.
3 · RIGHT / FULL OUTER JOIN (rare)
-- right
SELECT * FROM a
RIGHT JOIN b ON b.a_id = a.id;
-- full
SELECT * FROM a
FULL JOIN b ON b.a_id = a.id;
Use sparingly; often a UNION of two LEFT JOINs is clearer.
4 · WHERE vs ON filtering
-- filter before join
SELECT * FROM a
JOIN b ON b.a_id = a.id AND b.active;
-- filter after join
SELECT * FROM a
JOIN b ON b.a_id = a.id
WHERE b.active;
ON filters rows before they join, reducing work; WHERE filters the joined result. Know the difference or you’ll change row counts.
5 · Good habits
- Qualify every column (
table.col)—future schema edits won’t break queries. Official doc - Index join keys on both sides.
- Check row counts before & after join when debugging.
ASCII refresher
INNER LEFT RIGHT FULL
a ∩ b a ⊆ a⋃b b ⊆ a⋃b a⋃b
Command‑line Basics
You’ll spend more time in a terminal than any GUI, so this chapter is the crash‑course that gets muscle memory on point:
- Core UNIX Commands:
pwd env cd ls ssh mkdir cp mv rm cat grep sqlx cargo. - Shell & Prompt Tooling:
zsh, Oh My Zsh, and Homebrew.
Run every snippet as you read—trying beats memorising.
Core UNIX Commands
The table below is 80 % of what you’ll type each day. Flags shown are the safe defaults we review in PRs.
| Command | Cheat‑description | Example |
|---|---|---|
pwd | prints full path of current directory | pwd |
env | dumps environment variables (useful for debugging config) | env | grep DATABASE |
cd | change directory (cd ‑ jumps back) | cd ~/projects |
ls ‑la | list files, humans size, dotfiles visible, colorized by default | ls -la ~/Downloads |
mkdir ‑p | make directory, parents if needed | mkdir -p tmp/cache |
cp ‑a | copy file/dir, preserve perms (-r not needed with ‑a) | cp -a src build/ |
mv | move or rename file/dir | mv foo.txt bar.txt |
rm ‑rf | delete recursively, no prompts; triple‑check path first! | rm -rf build/ |
cat | dump file to stdout (pipe into less -R for paging) | cat README.md |
grep ‑r | search recursively for string in files | grep -r 'foo' src/ |
ssh -v user@host | secure shell; -v prints handshake debug | ssh -v dev@10.0.0.5 |
sqlx migrate add <migration-name> | create a new migration file with the given name. | sqlx migrate add init-user |
cargo check | check the code for errors without building it. | cargo check |
cargo fmt | format the code according to the Rust style guide. | cargo fmt |
cargo clippy | lint the code to find potential issues. | cargo clippy |
cargo build | build the code. | cargo build |
cargo run | run the code. | cargo run |
cargo test | run the tests. | cargo test |
cargo run -p <package> | run a specific package. | cargo run -p api |
cargo add <package> | add a new package to the project. | cargo add tokio -F full |
Further reading
- “pwd” Linux man page
- “env” command usage guide on Geeks‑for‑Geeks :contentReference[oaicite:1]{index=1}
- PhoenixNAP’s
cdprimer :contentReference[oaicite:2]{index=2} - “ls” Linux man page :contentReference[oaicite:3]{index=3}
- “mkdir(2)” Linux man page :contentReference[oaicite:4]{index=4}
- Wikipedia’s overview of
cp:contentReference[oaicite:5]{index=5} - LinuxCommand.org lesson on moving files :contentReference[oaicite:6]{index=6}
- Tutorialspoint
rmcommand guide :contentReference[oaicite:7]{index=7} cat(1)Linux man page :contentReference[oaicite:8]{index=8}- freeCodeCamp tutorial on
grep:contentReference[oaicite:9]{index=9}
Shell & Prompt Tooling
zsh is the default on modern macOS and many Linux distros. Stick to it; completions and plugins are better than old‑school bash.
1 - Install Oh My Zsh
Run the official one‑liner installer; it clones a git repo into ~/.oh‑my‑zsh and appends bootstrap code to ~/.zshrc. Enable plugins we lint in CI:
plugins=(git rust docker)
Reload with source ~/.zshrc. Oh My Zsh auto‑updates weekly unless you disable it.
2 - Homebrew
Homebrew is our cross‑platform package manager. The install script detects Apple Silicon vs Intel and places binaries under /opt/homebrew or /usr/local accordingly. Post‑install, brew doctor checks your PATH.
brew install bat
brew upgrade && brew autoremove
3 - Add an alias for cat
bat is a drop‑in replacement for cat with syntax highlighting and line numbers. It’s a great way to view files in the terminal.
nano ~/.zshrc
# Add this line to your ~/.zshrc file
# to use bat as a replacement for cat
# This will show line numbers and colorize the output
# when you use the cat command
alias cat='bat --style=numbers --color=always'
3.3 - Tips
cat -p # Use -p to disable formatting and show raw output
4 - Make zsh your default (macOS)
chsh -s $(which zsh)
Rust Basics
Rust is a compiled, statically typed language that focuses on safety and zero‑cost abstractions. By the end of this primer you’ll recognise the core syntax, read production code, and write simple functions—ready for deeper topics like ownership, lifetimes, and async.
Variables & Mutability
fn main() { let x = 5; // immutable by default let mut y = 5; // mutable y += 1; println!("{x} {y}"); }
Values are immutable unless you add mut. Shadowing (let x = x + 1;) lets you reuse a name while leaving the original binding unchanged.
Data Types
#![allow(unused)] fn main() { let a: i32 = 42; // 32‑bit signed let b: f64 = 3.14; // 64‑bit float let c: char = '🦀'; // Unicode scalar let d: bool = true; let tup: (i32, f64, bool) = (a, b, d); let (x, y, z) = tup; // destructuring let arr = [1, 2, 3]; // fixed‑size array (stack) let mut vec = vec![1, 2, 3]; // growable vector (heap) }
Functions
fn add(a: i32, b: i32) -> i32 { a + b // implicit return (no semicolon) } fn main() { println!("{}", add(2, 3)); }
A function starts with fn; parameters are type‑annotated. The last expression, without a semicolon, becomes the return value.
Comments
#![allow(unused)] fn main() { // single‑line /* block */ //! crate‑level documentation /// item‑level docs collected by `rustdoc` }
Line comments explain why, not what. Doc comments (/// and //!) generate HTML docs via cargo doc.
Control Flow
#![allow(unused)] fn main() { for n in 1..=5 { if n % 2 == 0 { println!("{n} even"); } } let mut i = 0; while i < 3 { i += 1; } }
Rust offers if/else, while, and for; any loop can break or continue.
Structs
struct User { id: u32, email: String, active: bool, } fn main() { let mut user = User { id: 1, email: "a@b.com".into(), active: true }; user.active = false; }
Structs bundle related fields. The whole instance must be mutable to change any field.
Rust’s structs Rust’s struct design patterns
Methods
#![allow(unused)] fn main() { impl User { fn deactivate(&mut self) { self.active = false; } } }
Methods live in impl blocks and take self, &self, or &mut self as the first parameter.
Enums & Pattern Matching
#![allow(unused)] fn main() { enum Direction { North, East, South, West } fn compass(dir: Direction) { match dir { Direction::North => println!("Up"), Direction::South => println!("Down"), _ => println!("Sideways"), } } }
match is exhaustive, forcing you to handle every variant. Use if let or let else for concise single‑pattern checks.
Rust’s enums Rust’s match Rust’s if let
Packages, Crates & Modules
cargo new hello-cli # binary crate
cargo new util --lib # library crate
A package is one or more crates plus Cargo.toml. Use mod to declare modules and use to bring items into scope; split large modules into multiple files for clarity.
#![allow(unused)] fn main() { mod db { pub fn connect() {} } use crate::db::connect; }
Error Handling
#![allow(unused)] fn main() { fn read() -> std::io::Result<String> { /* ... */ } if read().is_err() { eprintln!("failed"); } match read() { Ok(text) => println!("{text}"), Err(e) => eprintln!("error: {e}"), } }
Use is_err() for quick checks; prefer match (or the ? operator) when you need the Ok value or fine‑grained error logic. Reserve panic! for unrecoverable bugs.
Rust’s error handling Rust’s Result type Rust’s ? operator
Generics, Traits & Lifetimes — a preview
#![allow(unused)] fn main() { fn largest<T: PartialOrd>(slice: &[T]) -> &T { let mut max = &slice[0]; for item in slice { if item > max { max = item; } } max } }
Generics enable type‑agnostic code, traits define shared behaviour, and lifetimes tell the compiler how references relate. You’ll dive deeper in later chapters.
Rust’s generics Rust’s traits Rust’s lifetimes
Ownership & Borrowing
1 - The Three Rules of Ownership
- Every value has one owner.
- When the owner goes out of scope, the value is dropped.
- A value can be borrowed by reference, but while it’s mutably borrowed no other borrows are allowed.
These rules are enforced at compile‑time, so there’s zero runtime overhead.
2 - Move Semantics
fn main() { let s1 = String::from("hello"); // s1 owns heap data let s2 = s1; // ownership moved; s1 invalid // println!("{s1}"); // 🔴 compile‑error: value borrowed after move println!("{s2}"); // ✅ ok let n1 = 5; // i32 implements Copy let n2 = n1; // bits duplicated; both variables usable println!("{n1} {n2}"); }
Types with the Copy trait (numbers, bools, chars, arrays of Copy) are duplicated; everything else moves by default.
3 - Immutable Borrowing
fn main() { let data = String::from("rust"); let len = length(&data); // borrow read‑only println!("{data} has length {len}"); } fn length(s: &String) -> usize { s.len() }
Any number of &T (immutable) references may coexist—perfect for read‑heavy scenarios.
4 - Mutable Borrowing
fn main() { let mut buf = String::from("hi"); shout(&mut buf); // exclusive access println!("{}", buf); // "HI" } fn shout(s: &mut String) { s.make_ascii_uppercase(); }
A &mut T gives one writer exclusive control. No other borrows (mutable or immutable) may exist in the same scope.
4.1 - Why Mixed Borrows Fail
#![allow(unused)] fn main() { let mut v = vec![1, 2, 3]; let item = &v[0]; // immutable borrow v.push(4); // 🔴 cannot borrow `v` as mutable because it’s also borrowed as immutable println!("{item}"); }
Rust prevents reads that could point to reallocated memory after push().
Fixes: shorten the immutable borrow’s scope, clone the data, or restructure code.
5 - Lifetimes in 60 Seconds
A lifetime is the span during which a reference is valid. The compiler tags each reference with a lifetime parameter ('a, 'b, …) and makes sure they don’t outlive the data they point to.
#![allow(unused)] fn main() { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } }
Usually lifetimes are inferred; you specify them when multiple borrows overlap and Rust can’t guess.
6 - Slices: Borrowing Parts of Collections
#![allow(unused)] fn main() { fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for i in 0..bytes.len() { if bytes[i] == b' ' { return &s[..i]; } } s } let sentence = String::from("hello world"); let word = first_word(&sentence); println!("{word}"); // "hello" }
A slice (&[T] or &str) borrows a contiguous range without copying.
7 - Interior Mutability & Shared Ownership
When you need multiple owners and mutation, reach for these tools:
| Tool | What it does |
|---|---|
Rc<T> | Single‑threaded reference counting |
Arc<T> | Thread‑safe reference counting |
RefCell<T> | Run‑time borrow checking (single thread) |
Mutex<T> / RwLock<T> | Mutual exclusion for data inside Arc |
Example: mutate across threads with Arc<Mutex<T>>.
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let c = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut num = c.lock().unwrap(); *num += 1; })); } for h in handles { h.join().unwrap(); } println!("counter = {}", *counter.lock().unwrap()); }
The borrow checker trusts the Mutex at runtime to enforce exclusive access.
8 - Common Compile‑Time Errors & Fixes
| Error message snippet | Why it happens | Typical fix |
|---|---|---|
value borrowed here after move | you used a value after ownership transferred | clone or restructure code |
cannot borrow as mutable because it is also borrowed as immutable | a &mut overlaps an & | shorten scope, split function, or use interior mutability |
missing lifetime specifier | compiler can’t infer lifetimes | add explicit lifetime parameters |
9 - Hands‑On Exercises
- Fix the mixed‑borrow error
fn main() { let mut data = vec![1, 2, 3]; let first = &data[0]; data.push(4); println!("{first}"); }
- Return the biggest number without cloning
#![allow(unused)] fn main() { fn largest(slice: &[i32]) -> &i32 { // TODO } }