A constraint is a rule the database enforces for you. Instead of hoping every piece of application code remembers "email must be unique" or "a balance can't go negative", you declare it once on the table and Postgres guarantees it — rejecting any statement that would break it. Bad data never gets in.
The seed is a tiny bank: an accounts table where every column carries a rule, and a transactions table that points back to it.
SELECT * FROM accounts ORDER BY id;
NOT NULL: a value is required
The simplest constraint. A NOT NULL column must always have a value. The seed marks email as NOT NULL, so an insert that omits it fails:
INSERT INTO accounts (balance) VALUES (100);
The error names the column and constraint. Compare that to leaving a nullable column empty (like transactions.memo), which is perfectly fine.
PRIMARY KEY: the unique row identifier
A PRIMARY KEY marks the column(s) that uniquely identify each row. It's really two constraints in one: UNIQUE + NOT NULL. The seed's id is the primary key, so no two accounts can share an id and none can be null. A table gets at most one primary key — it's the identity of a row.
UNIQUE: no duplicates
UNIQUE forbids duplicate values in a column (or combination of columns) — without making it the row's identity. The seed makes email unique, so a second account with an existing email is rejected:
INSERT INTO accounts (email, balance) VALUES ('ada@example.com', 50);
One subtlety: UNIQUE allows multiple NULLs, because in SQL no two NULLs are considered equal. If you need "unique and always present", pair UNIQUE with NOT NULL (or use a primary key).