What’s new
Updates and improvements to Whal. Newest first.
- SecurityImprovementFixHighlighted
A faster, safer Whal — performance sweep, private damage photos, and nav polish
The morning's multi-tenant release was just the warm-up. This afternoon a long backlog of performance, security, and polish work landed in one push — the kind of changes you'll feel without being able to point at a single new button.
Dashboards that feel instant
The admin dashboard has been the slowest page in the app for months. That's over. Three things happened at once:
- Shared connection pool. Every dashboard card used to open its own Neon connection; now they share a warm WebSocket pool with a bounded LRU so the third and fourth cards inherit the handshake the first one paid for.
- One query instead of six. The KPI cards that used to each run their own aggregate now read from a single collapsed scan. Pallets, cartons, clients, LAL, occupancy, today's inbound, today's outbound, pending dispatches — one trip to the database.
- Tiered caches. Tenant resolution, warehouse config, and the "which slots exist" lookup are cached with per-key TTLs so a hot dashboard reload doesn't hammer the same rows.
Vercel is also now pinned to
syd1(Sydney), so every page load avoids a cross-ocean round trip. Speed Insights is wired in — you can watch the numbers yourself on the Vercel dashboard.Damage photos are now private
Before today, photos attached to inbound damage reports and stock-adjustment evidence lived on a public CDN with a guess-resistant URL. That's still leaky — the URL only has to escape once (a log entry, a screenshot, a forwarded email) to become permanently accessible. Driver signatures, shipping documents, customer addresses, bonded-goods labels: all potentially exposed with no revocation path.
New uploads to damage and adjustment folders now land on private storage and are served only through the tenant-scoped proxy routes the admin UI already used. Anyone trying the underlying URL directly gets nothing. Legacy photos already in the system continue to render through the same proxy so nothing breaks.
Product photos stay public on purpose — those are catalogue assets that render on public product pages.
Stricter uploads
- Magic-byte sniffing. The upload endpoint no longer trusts the browser's claim about what type a file is. It reads the first bytes and rejects anything whose content doesn't match its declared MIME, stopping an SVG-disguised-as-PNG from slipping through.
- Per-user upload budget. 30 uploads per minute per user. Well above any realistic receipt-photo burst, well below a runaway script or stolen session.
- No direct deletes. The old delete endpoint accepted any blob URL from any session. It's been removed. Photo cleanup now goes through the server action that owns the row (receipt, adjustment, product) and verifies tenant ownership before touching storage.
- Allow-listed folders. Uploads can only target four known folders. No more path-traversal-style shenanigans.
Navigation that knows where you are
The top bar has been relabelled so every page title matches the sidebar item you clicked to get there — no more "Inbound (FSM)" mystery or blank titles on deep pages. The mobile bottom nav now includes Transfers, Batches, and Alerts under "More" so you're not forced to open the hamburger for a two-tap jump.
Alerts' empty state stopped saying "The warehouse is quiet. ☕" — cute, but a warehouse with zero alerts is probably a warehouse with a broken alert pipeline; the new copy just reads "No alerts match this filter."
Fewer moving parts on the admin surface
- Setup wizard retired. Every capability the wizard exposed is reachable from the regular settings pages, and the wizard's mid-flow state could desync from reality. Gone.
- Stocktakes removed. The feature was half-built and nobody used it. Cycle counts via adjustments cover the same ground.
- Transfer fees are billable. Movements between warehouses can now carry a per-transfer fee that flows through the normal charge/invoice pipeline.
Outbound for Standard warehouses
The outbound orders table has been unified so Standard (non-bonded) warehouses get the same order lifecycle the bonded flow has always had. Excise rates are snapshotted onto the order at allocation time — so a rate change between allocation and dispatch can't quietly shift what the customer pays.
Related vocabulary sweep: labels and empty states that used to say "bonded" in places where the feature works identically for Standard warehouses now use neutral terms (movements, dispatches, stock) so Standard tenants don't wonder whether the feature applies to them. Internal schema names are unchanged; this is a UI-only pass.
What this means for you
Nothing to do. Existing sessions keep working. If you've been dreading the dashboard load time, give it another try — it should feel different. If you work with damage photos, the URL structure has changed (they're served from
/api/photos/...under your session) but the UI is identical. - FeatureFixSecurityHighlighted
Multi-tenant hardening, image uploads, and a fully furnished demo
A big day for the platform. Tenant isolation is now airtight, image uploads are live, the demo account got a complete makeover, and subdomains register themselves.
Automatic subdomain registration
New tenants no longer need a manual trip to the Vercel dashboard. When someone signs up, Whal calls the Vercel Domains API to register their subdomain automatically —
slug.whal.com.auis live by the time they finish the wizard. If the API is unreachable, signup continues and the domain can be added later.Image uploads via Vercel Blob
Three places in the app now support image uploads backed by Vercel Blob CDN storage:
- Products — upload a product photo from the product form (compact camera icon next to Category).
- Stock adjustments — attach evidence photos when recording damage, breakage, or corrections.
- Inbound receiving — document delivery condition with multiple photos per receipt.
Images are drag-and-drop or click-to-browse, with thumbnail previews and one-click removal. Files are validated for type and size (5 MB max) before upload.
Tenant isolation fixes
The warehouse config reader had an unsafe fallback: when the session's
tenantIdwas missing, it fell back toSELECT * FROM warehouse_config LIMIT 1with no tenant filter — silently reading another tenant's data. This was the root cause of warehouse types showing incorrectly across tenants.Both
getWarehouseConfig()andsaveWarehouseConfig()now refuse to operate without a valid tenant scope. Additionally, the JWT callback now backfillstenantIdfrom the database for sessions created before multi-tenancy was added, so stale tokens self-heal on the next request.Branded dashboard header
The dashboard header now shows the tenant's logo (compact pill mark), business name, and a Bonded/Standard badge — giving each warehouse its own identity instead of a generic page title.
Fully furnished demo warehouse
The demo account has been rewritten from scratch. Logging in as
demo@whal.com.aunow drops you into Sandbox Spirits Warehouse — a complete bonded warehouse with:- 3 clients (Sandbox Spirits, Coastal Distillery, Mountain Creek Wines)
- 16 products across gin, vodka, whisky, rum, liqueur, RTD, and wine
- A 4-zone warehouse layout with ~120 slots across 7 rows
- 30 pallets placed into specific locations, including one on hold and one quarantined
- 8 weeks of movement history, invoices, charges, dispatch requests, and alerts
The demo reset wipes all three clients and all tenant-scoped data (layout, config, fees, settings) before reseeding — so every visitor gets a clean warehouse.
Other fixes
- Trial banner hidden for demo users — the demo tenant doesn't have a real subscription, so "Trial · 90 days left" no longer appears.
- Trial banner hidden for super admins — platform operators don't need billing nags.
- Setup wizard pre-populates from existing config instead of overriding your signup choices with defaults.
- Signup form now defaults to "Bonded" warehouse type to match the DB default.
- SecurityHighlighted
Security hardening — rate limits and stricter tenant fences
A round of quiet-but-important security work landed this week. Nothing visible during normal use, but a few ways Whal could be pushed around are now closed off.
Rate limiting on login
We now throttle login attempts both per email and per IP address. Five bad tries against the same account in fifteen minutes pauses the account briefly; the same window per IP catches automated credential-stuffing bots. The demo account is exempted so marketing demos always succeed.
The limiter is Postgres-backed and fails open — if the limiter itself has a problem, logins continue to work rather than locking everyone out.
Tighter tenant isolation across the admin surface
Every action that reads or writes tenant data now goes through a single, shared helper that checks the caller's tenant before the database is touched. A few admin-level functions had been taking a client ID from the request and trusting it without a follow-up ownership check — those have been closed. A scoped admin (for example, the demo account) can no longer list another client's products, pallets, invoices, or dispatch orders, and can no longer push another tenant's invoices to Xero or MYOB.
Billing rollup is platform-admin only
The monthly billing rollup runs across every tenant in the period. The permission check has been tightened so only platform admins — not tenant-scoped admins — can trigger preview or commit.
What this means for you
Nothing to do. Existing sessions keep working; the changes are behind the scenes. If you're an ops user, you may notice that logging in with a bad password a handful of times now pauses you for fifteen minutes — the message is the same as a wrong-password error so attackers can't tell the difference.