Tiny analytics for micro apps: using ClickHouse and TypeScript to measure engagement
Build privacy-friendly, typed analytics for micro apps with TypeScript events and ClickHouse. Fast ingestion, ad-hoc SQL, and practical recipes for 2026.
Hook — you built a micro apps. Now what about analytics?
Micro apps are nimble: fast to build, easy to iterate, often personal or shared with a handful of users. But the next common pain point is measuring what matters without shipping heavy, privacy-invasive tooling. You want lightweight, privacy-friendly analytics that give you ad-hoc insights fast — but you also want typed safety so your instrumentation doesn’t rot as the app evolves. This guide shows how to write typed events in TypeScript, ingest them into ClickHouse, and build fast, low-cost dashboards for micro apps in 2026.
Why this matters in 2026
Two forces shape this moment: the rise of micro apps (personal, ephemeral, and often maintained by single developers or small teams) and the maturation of modern OLAP engines like ClickHouse. ClickHouse’s rapid growth and funding in late 2025 pushed a wave of richer cloud integrations, better connectors (Kafka, HTTP), and optimized storage engines for low-latency analytical workloads. For micro apps, this means you can:
- Collect detailed event data at low cost using serverless or managed ClickHouse offerings.
- Run ad-hoc SQL queries and power dashboards for product decisions without provisioning a heavy analytics stack.
- Keep data collection privacy-friendly by doing hashing/aggregation client-side and preserving minimal PII.
“Micro apps are personal and fleeting. Analytics for them must be equally nimble, respectful of privacy, and typed so instrumentation remains reliable.”
Design goals for tiny analytics
Before code or schema, decide goals. For micro apps I recommend:
- Minimal payloads: small events to save bandwidth and cost.
- Typed events: TypeScript types + runtime validators to avoid schema drift.
- Privacy-first: avoid storing raw PII; use ephemeral IDs or hashing.
- Ad-hoc analysis: store raw-ish events in ClickHouse so SQL ad-hoc queries remain fast.
- Cost-efficiency: batch client sends, use ClickHouse TTLs and partitions.
Event modeling with TypeScript (and runtime validation)
Static types are great, but runtime validation is required for ingestion. Use a small, dependable validator like zod (or io-ts / superstruct) so your TypeScript types and runtime checks are aligned.
Example: typed events for a micro app (Where2Eat)
import { z } from 'zod'
// Discriminated union of events
export const SessionStart = z.object({
type: z.literal('session_start'),
version: z.number().int().default(1),
sessionId: z.string(),
startedAt: z.string(), // ISO
optInAnalytics: z.boolean().default(true),
})
export const Recommend = z.object({
type: z.literal('recommend'),
version: z.number().int().default(1),
sessionId: z.string(),
query: z.string(),
resultCount: z.number().int(),
elapsedMs: z.number().int(),
})
export const Vote = z.object({
type: z.literal('vote'),
version: z.number().int().default(1),
sessionId: z.string(),
restaurantId: z.string().optional(),
vote: z.union([z.literal('up'), z.literal('down')]),
})
export const AppEvent = z.discriminatedUnion('type', [SessionStart, Recommend, Vote])
export type AppEvent = z.infer
Key practices:
- Discriminated unions (type field) make downstream routing and storage simple.
- Version fields allow additive changes while keeping parsing resilient.
- Prefer ISO strings for dates and convert to ClickHouse DateTime64 at ingestion.
From typed events to ClickHouse: ingestion patterns
ClickHouse supports several ingestion patterns. For micro apps, pick the simplest that matches your deployment:
- Direct HTTP Insert: client or small server POSTs batched JSON/CSV to ClickHouse’s HTTP endpoint (suitable for low to moderate traffic).
- Server-side ingest + ClickHouse client: use a tiny Node.js server to validate, enrich, and forward events (recommended when you want to filter PII or apply business rules).
- Kafka or Kinesis: stream to a broker and let ClickHouse’s Kafka engine consume—useful when queues are needed.
Minimal serverless ingestion flow
- Client batches events (5–50 events) and POSTs every 10–30s.
- Serverless function validates events using zod, removes PII, adds arrival_ts, and forwards via ClickHouse HTTP insert.
- ClickHouse stores raw events in a MergeTree table partitioned by day.
Mapping TypeScript types to ClickHouse DDL
Here’s a simple MergeTree DDL for the example events. It stores the raw JSON and extracts key columns for fast queries.
CREATE TABLE app_events (
event_date Date DEFAULT toDate(arrival_ts),
arrival_ts DateTime64(3),
event_type String,
event_version UInt32,
session_id String,
payload String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, session_id, arrival_ts)
TTL arrival_ts + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
Why this shape?
- Store payload as JSON string to keep schema flexible while indexing a few typed columns for fast queries.
- Partition by month and TTL after 90 days by default — sensible for micro apps that don’t need long-term raw storage.
- Order by (date, session_id, arrival_ts) optimizes session-level queries and time-range scans.
Runtime ingestion example: Node.js serverless function
import express from 'express'
import fetch from 'node-fetch'
import { AppEvent, AppEvent as AppEventSchema } from './events'
const app = express()
app.use(express.json())
app.post('/ingest', async (req, res) => {
const events = Array.isArray(req.body) ? req.body : [req.body]
const good: string[] = []
for (const raw of events) {
const parsed = AppEventSchema.safeParse(raw)
if (!parsed.success) continue
const e = parsed.data
// Privacy step: drop or hash PII fields
if (!e.optInAnalytics) continue
const row = {
arrival_ts: new Date().toISOString(),
event_type: e.type,
event_version: e.version || 1,
session_id: e.sessionId,
payload: JSON.stringify(e),
}
good.push(Object.values(row).map(v => JSON.stringify(v)).join(','))
}
if (good.length === 0) return res.status(204).end()
// Send CSV lines to ClickHouse HTTP
const body = good.join('\n')
const url = process.env.CLICKHOUSE_URL + '/?query=INSERT%20INTO%20app_events%20FORMAT%20CSV'
await fetch(url, { method: 'POST', body, headers: { 'Content-Type': 'text/csv' } })
res.status(200).json({ ingested: good.length })
})
app.listen(3000)
Notes:
- CSV is compact; JSONEachRow is another option.
- Batching client-side reduces request count and improves cost-efficiency.
- Use the ClickHouse HTTP(s) endpoint and secure it via credentials or ephemeral tokens.
Privacy-first techniques for micro apps
Micro apps are often personal; users must be able to trust you. Practical privacy patterns:
- Consent flags: include optInAnalytics on events. Don’t collect if false.
- Hash identifiers: apply a one-way hash (SHA-256 truncated) with a per-app salt stored server-side, not the user’s email or phone.
- Client-side aggregation: compute non-identifying aggregates (counts, histograms) on the client and send metrics instead of events when possible — this is a useful pattern when you’re considering on-device computation to avoid shipping PII.
- Ephemeral session IDs: rotate session IDs periodically to reduce cross-session linking.
- Data minimization: record only what you need. Avoid storing full query strings if you can bucket them.
Schema evolution and versioning
Change is constant. For robust analytics:
- Include version on each event.
- Favor additive changes. New fields are optional; older parsers ignore them.
- Keep the raw payload so you can run backfill parsing when you deploy new parsers — storing the raw payload makes it easier to run an evidence capture-style reparse later.
- Consider a lightweight schema registry: store the TypeScript type definitions or zod schemas in a Git repo and tag releases. For larger teams, publish JSON Schema artifacts to a registry.
Fast ad-hoc analysis and dashboards
ClickHouse shines for ad-hoc SQL. Example queries you’ll run often:
Sessions per day
SELECT event_date, countIf(event_type = 'session_start') AS sessions
FROM app_events
WHERE event_date BETWEEN today()-14 AND today()
GROUP BY event_date
ORDER BY event_date
Median response time for recommendations
SELECT quantileExact(0.5)(toInt32(JSONExtractInt(payload, 'elapsedMs'))) AS p50
FROM app_events
WHERE event_type = 'recommend'
AND event_date BETWEEN today()-7 AND today()
Use Grafana (ClickHouse datasource plugin) or Apache Superset for lightweight dashboards and quick SQL exploration. In 2026, Grafana’s ClickHouse integration supports direct SQL exploration and templated dashboards, making it the fastest way to add charts for a micro app.
Operational tips — batching, partitioning and costs
Small apps still need operational discipline:
- Batching: send 10–50 events per request; balance latency vs cost.
- Partitioning: partition by month (toYYYYMM) for cheap pruning and fast range queries.
- TTL: automatically drop raw events after 30–90 days, depending on your needs.
- Compression: ClickHouse’s codecs are efficient. Use default settings unless you run into cost issues.
- Sampling: for very high-volume apps, sample events client-side (e.g., 1% globally, 100% for errors) and record the sampling rate so you can scale counts up in SQL.
Putting it together — minimal end-to-end example
High-level steps to ship a tiny analytics pipeline in a weekend:
- Create TypeScript event schemas (zod).
- Build a small ingestion endpoint (serverless) that validates, redacts PII, and forwards to ClickHouse HTTP insert.
- Deploy a MergeTree table with partitioning and TTL.
- Use Grafana or Superset for dashboards and quick SQL exploration.
Starter checklist (copy-paste):
- events.ts with zod schemas + tests
- serverless function with /ingest route
- ClickHouse DDL deployed (app_events table)
- Grafana board with sessions, p50 latency, and retention chart
Advanced strategies & 2026 trends
Looking forward, several trends matter for tiny analytics:
- Edge-first capture: collecting and aggregating data at the edge (browser or device) before sending reduces latency and privacy exposure.
- Privacy-enhancing tech: differential privacy and secure aggregation tools are becoming accessible; expect lightweight libraries that add DP noise at the client.
- ClickHouse ecosystem growth: since late 2025, ClickHouse’s cloud offerings and connectors matured — expect tighter integrations with ML services for quick, model-backed analytics on event streams.
- Serverless OLAP: on-demand compute for analysis queries reduces the need to run a full BI cluster; many ClickHouse cloud plans now offer per-query scaling, and messaging backbones like Telegram power small-scale event-driven workflows for pop-ups and micro-events.
Common pitfalls and how to avoid them
- Too much raw data: store only what you need; use TTLs.
- No runtime validation: leads to silent schema breakage; use zod/io-ts and tests.
- PII leaks: never send raw emails/phones; hash or omit them.
- Poor partition keys: avoid high-cardinality partition keys; stick to date and a low-cardinality secondary key.
Actionable takeaways
- Start with a typed event contract in TypeScript and a runtime schema (zod).
- Batch events client-side and use a tiny serverless function to validate and remove PII.
- Ingest into a ClickHouse MergeTree table that stores a small set of indexed columns and a JSON payload for flexibility.
- Partition by month, set a 30–90 day TTL, and build Grafana dashboards for ad-hoc analysis.
- Monitor costs: use sampling and compression when traffic grows.
Conclusion — ship safe, stay private, iterate fast
Micro apps don’t need heavyweight analytics stacks to get meaningful product insights. By combining TypeScript-typed events, lightweight runtime validation, and the power of ClickHouse, you can build a tiny analytics pipeline that is fast, cheap, and respectful of user privacy. With the ClickHouse ecosystem continuing to mature in 2026, now is a great time to adopt this approach for personal projects or small-scale production apps.
Want a starter repo with TypeScript schemas, a serverless ingestion function, and ClickHouse DDL you can deploy this afternoon? Click the link below to get a ready-to-run template and step-by-step instructions.
Call to action: Try the starter template, instrument one event, and build your first Grafana chart — then iterate on schema and privacy settings based on real usage.
Related Reading
- Integration Blueprint: Connecting Micro Apps with Your CRM
- Local-First Edge Tools for Pop-Ups and Offline Workflows
- Storage Considerations for On-Device AI and Personalization (2026)
- Edge Migrations in 2026: Architecting Low-Latency MongoDB Regions
- Bluesky’s New Live Badges and Cashtags: How Creators Should Respond to Platform Migration
- Top 10 Crossover Collectibles of 2026: MTG, LEGO and Nintendo Items You Can’t Miss
- From Garden Knees to Custom Fit: 3D-Scanning for Personalized Knee Pads and Tool Handles
- Options Strategy Workshop: Using Collars to Protect Precious Metal Gains Post-Large Sales
- Designing an AirDrop-Compatible Hardware Module: Bluetooth, UWB, and Peer-to-Peer Protocols for Mobile OEMs
Related Topics
Unknown
Contributor
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you
Voice + VR + TypeScript: prototyping an assistant for virtual meeting rooms
Streamlining Game Verification: Lessons from Steam's New Approach
Contracts first: using TypeScript type generation from analytics schemas (ClickHouse/Parquet)
Exploring Tab Grouping Techniques in TypeScript for Enhanced User Navigation
AI Integration: Tailoring Gemini for TypeScript in App Development
From Our Network
Trending stories across our publication group