AE Internal App
The student union's culture section lends speakers, chairs and projectors to every club, and it needs to know at any moment what is on the shelf and what is out. The AE Internal App is where the union's teams sign in by role, and its first module is the culture warehouse: what exists, what is out, who took it and why.
AE INTERNAL APP · SYNTHETIC DATA ON LOCAL EMULATORS
Why this exists
Every item the culture section owns is either on a shelf or out with a club, and the section needed one place that said which, with a record of who took it and when, so that a shortage shows up before the event and not on the day. The union also needed to control who can see and change any of this, because its members rotate every year.
1 # What was broken
2
3 - no single view of shelf vs. lent out
4 - loans and returns need a signed record
5 - shortages must show up before the event
6 - members change every academic year
7
8 > goal: physical vs. available, always known
The decisions I made
Each item keeps two numbers, total and available, because a speaker lent to a club still belongs to the union. Access is decided in Firestore rules from the member's section and role, not in the interface: presidency and culture coordinators can change stock, culture members can only read, everyone else sees nothing. Sign-in is Google restricted to the union's domain, and a new member waits as pending until a coordinator assigns a role. Two Cloud Functions expose the stock to the clubs portal, and a third emails the section when an item drops below its minimum.
1 Angular SSR ──▶ Firestore ◀── rules by section + role
2 │
3 inventario_cultural · transacoes_armazem
4 │
5 alertLowStock ──▶ email below minimum
6 getInventory / syncInventory ◀── clubs portal
7
8 // trade-off: permissions live in the rules, not the UI
The part I'm proud of
Every stock change happens in one Firestore transaction that updates the item and writes the movement log together: either both land or neither does. The transaction refuses anything that would take stock below zero, and the rules make the log append-only, so even an admin cannot quietly edit history afterwards.
1 await runTransaction(db, async (tx) => {
2 const novaDisp = data.quantidadeDisponivel + diff;
3 if (novaDisp < 0) throw new Error('Stock não pode ser negativo.');
4 tx.update(itemRef, { quantidadeDisponivel: novaDisp });
5 tx.set(transRef, log); // same commit
6 });
7
8 // firestore.rules → update, delete: if false
What changed afterwards
The culture section has one screen that answers what is in the warehouse, what is out with which club, and what is about to run out, with every movement signed by the member who made it. The clubs portal reads the same stock, so a club sees what is available before it asks. What I would do differently: the sync endpoint matches items by name, so renaming an item in the warehouse quietly breaks the link; it should use the item ID, and the API key should never reach the browser.
1 ## Shipped in v1
2
3 stock total vs. available, by location
4 history append-only movement log
5 alerts email below the minimum
6 access Google sign-in · roles by section
7
8 ### What I'd do differently
9 match by ID, not by name