Version 1.0.5 is the largest engineering release BarlaPost has shipped. It rebuilds the analytics surface, roughly halves what the app downloads, and reworks how the API resolves clients and survives load. Every size figure below was measured by comparing two production Expo exports, not estimated.
Stats, rebuilt around charts
The old Stats screen had the data and spent it on 48-by-18-pixel sparklines. Every metric already arrived with its daily series, its engagement components and a next-sync timestamp; none of it was drawn.
Stats is now two screens. The overview shows the whole workspace, then each connected account as a row into its own detail screen.
The overview deliberately does not sum raw platform metrics. "Views" plus "impressions" plus "reach" is not a quantity anyone can name, so the three numbers on offer are the ones every platform can answer: reach-like impressions, interactions, and the engagement rate between them. Each account contributes whichever of its metrics plays that role — YouTube counts views, X counts impressions, Instagram counts reach.
Two derivations are worth spelling out, because the obvious implementations are both wrong:
- The engagement rate is recomputed from the totals, never averaged. Averaging per-account rates lets a small account drag the workspace figure around.
- The period-over-period delta is weighted, not averaged. The API returns only the current window's series, but each metric's delta was computed server-side as (current − previous) ÷ previous × 100, so inverting it recovers the exact previous total. Those are summed and divided. Averaging percentages is how one account going from 1 to 5 views writes "+400%" across an entire workspace.
The per-account screen is where platform-native metric names are safe to show, because inside a single platform "views" means exactly one thing. It carries a metric selector, a daily area chart you can drag across to read any day's value, a KPI grid, a horizontal breakdown of what the engagement was made of, and a ranked list of the posts that carried the window.
That last one is backed by a new mobile API endpoint, GET /api/mobile/v1/workspaces/{id}/accounts/{account_id}/analytics/posts/. It is a thin envelope around the same service function the web analytics table already used, so a post's numbers read identically on both surfaces. It is account-scoped rather than workspace-wide on purpose: post metrics are not comparable across platforms, so a single merged ranking would sort on numbers that do not mean the same thing.
Charts are drawn with react-native-svg. A single series carries no legend — the heading names what is plotted — and labels only the peak, the two ends of the range, and whatever your finger is on. The scrub gesture waits until a drag is clearly horizontal, so a vertical swipe still scrolls the page.
The download is roughly half what it was
A production iOS export went from 12.68 MB to 5.59 MB — 7.09 MB smaller, or 56%. Three separate barrel imports were responsible.
Fonts fell from 5.32 MB to 0.92 MB. Every @expo-google-fonts family's index.js calls require on every weight it ships, so importing a single weight by name bundled all of them: 36 TTF files for the seven the app actually loads. Importing each weight's own module instead drops 4.40 MB.
Lucide icons fell from 1391 KB to 30 KB. The barrel re-exports roughly 3,500 icons, and Babel compiles those re-exports to eager require calls — so importing one icon executed thousands of module factories at startup. Per-icon imports fixed both the size and that startup cost. Four of the names in use are deprecated aliases (AlertTriangle is triangle-alert, Home is house), resolved from the barrel rather than guessed.
Runtime SVG parsing cost about 470 KB, plus a visible glitch. Platform badges used LocalSvg from react-native-svg/css, which reads the file over the bridge and parses XML and CSS on every mount with no cache — which is why badges appeared after the row around them had already drawn. It also pulled css-tree, entities and css-select into the bundle for ten logos. SVG now compiles to a React component at build time through react-native-svg-transformer; those three packages are gone entirely and nothing is parsed at runtime.
Moti cost about 512 KB. Moti is a declarative layer over Reanimated that ships framer-motion with it. All 19 animations in the app were either mount-time entrances or press scales, both of which Reanimated expresses natively as layout animations and shared values.
Total modules in the production bundle fell from 4,330 to 2,124.
Pages stopped shifting under you
Tab switching goes through a route replace, so every screen remounts on every visit — and replayed its whole staggered entrance. What read as an entrance on first launch read as the page sliding up and down each time you moved between tabs.
Screen blocks no longer animate in. What animates now is only what a deliberate action triggers: opening a dialog, toggling the calendar's search tray, stepping to another month.
Every request has a deadline
The fetch API has no timeout of its own, so a stalled connection — a captive portal, a phone that moved out of range mid-request, a server that accepted the socket and went quiet — left the promise pending forever and the screen spinning with no way back but killing the app.
Requests now fail after 20 seconds. Publishing gets its own 120, because it waits on the platforms themselves, several in a row, uploading media as it goes — but it is bounded, which is the point. An abort raises the same error every other transport failure does: a request that never answered and one that could not be sent are the same thing to the person waiting. File uploads are deliberately exempt, since their duration is set by the file's size and the phone's uplink.
The API serves four times as many requests at once
Gunicorn ran two workers of two threads, so the whole API served four requests concurrently and a fifth queued. Publishing is synchronous — it uploads media and talks to each platform in turn — so one publish can hold its thread for most of a minute. Four people posting a video simultaneously was enough to make the service unresponsive for everyone.
It now runs eight threads per worker: sixteen in flight. Threads rather than more workers, because the work is almost entirely waiting on PostgreSQL, object storage and the platforms' APIs; threads share the process's memory and, with persistent database connections disabled, idle ones hold no connections. The worker timeout was also raised to match the app's publish deadline — the default was below how long a legitimate publish takes — and access logs now reach stdout, which is the only way to notice abuse at all.
The Stats endpoint asks the database half as much
The overview walks every connected account's snapshot history, so its cost grows with the workspace. It was also asking for far more than it needed: one query per metric for the post-derived series, and one platform-configuration read per account, neither of which varies within a request.
Batched into a single scan per account and one configuration read per request, seven connected accounts went from 33 queries to 16. The per-metric freshness comparison is preserved — taking a single maximum across metrics would let one metric's fresh rows override another's newer ones.
The API moved to api.barlapost.com, behind Cloudflare
The mobile API now answers on a dedicated hostname served through Cloudflare, with WAF rate limiting in front of it. The previous hostname keeps working, so apps that have not updated are unaffected.
This surfaced a correctness bug that predated the migration. Every IP-keyed throttle trusts a fixed number of X-Forwarded-For entries counted from the right. Measured against the live deployment, a request arrives carrying the caller's address followed by the platform edge's own: the edge writes the caller first and a second internal hop appends itself. Trusting one entry from the right therefore returned the edge — the same value for every caller, and rotating between edge nodes request to request — so IP-keyed limiting was both shared and erratic. Behind Cloudflare the client address is read from CF-Connecting-IP instead, and only for requests that prove they arrived through the CDN.
Read endpoints are now throttled too. Analytics is the heaviest read the API serves and was the only surface without a limit, while every write endpoint already had one.
Platform availability
Connecting TikTok and Threads is temporarily unavailable. A connect flow builds its OAuth redirect URI from the host it was called on, and neither provider has the new callback registered yet. TikTok's is deliberate rather than pending: its content-posting audit is still open, changing an app's settings mid-review risks resetting it, and posting there is private-only until that clears. Accounts already connected keep publishing — that path uses stored tokens and never touches a redirect URI. Bluesky, Mastodon, LinkedIn, X and YouTube are unaffected.
Also in this release
- Six exported functions nothing imported, a screen no route could reach, and a duplicated component were removed. Every module in the app is now reachable from a route.
- The backend's lint and format tooling is pinned to one version across CI and local development; a floating range meant a developer on a newer release reformatted files that CI's older copy then rejected.
- Development and test dependencies no longer ship in the production container image.