# agustinusnathaniel.com - Full Content
================================================================================
BLOG POSTS (8)
================================================================================
---
title: 27 Life Lessons at 27
description:
date: 2026-02-13T00:00:00.000Z
tags: ['life', 'personal', 'reflections']
id: life-lessons-at-27
---
# 27 Life Lessons at 27
I wrote this when turned 27 about half a year ago. Turning 27 feels like a quiet reminder: three years before my 30s.
But I’m not here to count numbers. I just want to share some life lessons I’ve been holding onto from various experiences, events, and mistakes along the way.
I fully realize that not everything here will apply to everyone. I’m not trying to enforce principles or tell anyone how to live. These are simply personal reflections. If something resonates or gives you a small insight, that’s more than enough.
## 🌱 Life / General Lessons
1. **People often listen to what they are ready to hear.**
We cannot control interpretation. We can only control clarity and intention.
2. **Patience and calmness stem from anticipation and defined expectations.**
When we anticipate possible outcomes and define realistic expectations, we reduce unnecessary frustration. Uncertainty often fuels impatience.
3. **Luck = skill × opportunity (and timing).**
Skill prepares you. Opportunity appears. Timing amplifies. While many external factors exist, preparation increases the probability of luck.
4. **Better to fail than to do nothing.**
Failure gives feedback. Inaction gives regret.
5. **When someone needs you to listen, just listen.**
Not every situation or problems being told needs a solution. Sometimes presence and being heard is enough.
6. **History repeats. Look for patterns.**
In markets, relationships, and decisions. Patterns reduce surprises.
7. **If you cannot do it for yourself, do it for others. And vice versa.**
Responsibility can unlock strength we did not know we had.
8. **1.01^365 = 37.7**
Small daily improvements compound into massive change.
9. **Avoid overexplaining yourself.**
Clarity is good. Overdefending drains energy.
10. **Better to yield than escalate.**
Not every battle deserves your ego.
11. **Mistakes are powerful teachers.**
Failure is inevitable. Repeating it without reflection is optional.
12. **Be positive and share kindness.**
We never know what someone else is going through.
13. **Be bigger than the situation.**
Respond calmly. Do not let emotions dictate direction.
14. **24 hours can feel enough when priorities are clear and scope is realistic.**
Direction makes time feel sufficient.
15. **Do not let others dictate your life.**
Stay open to feedback, but own your decisions.
16. **Empty the glass.**
There is always something to learn. Ego blocks growth.
17. **Say no without guilt.**
Clear priorities make “no” responsible, not selfish.
18. **Rejection is redirection.**
Closed doors often prevent misalignment.
19. **Do not let one mistake erase all the good someone has done.**
People are more than their worst moments. One failure does not define a lifetime.
However, this does not mean ignoring serious wrongdoing. Accountability still matters. Forgiveness does not automatically restore trust, and repeated harm should never be justified.
20. **Not everything needs immediate expression.**
Process first. Write first. Observe first.
21. **Ultimately, we carry primary responsibility for our lives, even when external factors exist.**
Ownership builds momentum. Blame slows it down.
22. **When income increases, upgrade financial literacy.**
Learn about tax, insurance, and protection. Protect what you build — legally and wisely.
23. **Write things down.**
Writing organizes chaos. It clarifies emotion, logic, and conflict.
24. **Fix nutrition, stay hydrated and sleep early.**
Good bed, good pillow, enough water, protein, fiber, vitamins. Health is the base layer of everything else.
25. **Ask: “In the grand scheme of things, does this matter?”**
Perspective reduces unnecessary stress.
26. **Learn from anyone.**
Age and title do not guarantee wisdom.
27. **Avoid gossip.**
Praise publicly. Give constructive criticism privately.
## Additional Lessons
### 💼 Business / Professional Lessons
1. **Impact matters more than activity.**
It shows through leverage: delegating, mentoring, standardizing.
2. **Delegate intentionally.**
Guide when uncertain. Distribute responsibility wisely.
3. **Soft skills multiply hard skills.**
Communication, empathy, clarity, and articulation determine how far technical skill can scale.
4. **Knowing what, when, who, why, where, and how to ask is extremely valuable.**
Good questions reduce wasted effort.
5. **Strive to make wise and timely decisions.**
Speed matters. Judgment matters more.
6. **Take ownership and show up.**
Do not wait to be told what to care about.
7. **Seek clarity and unblock quickly.**
Ambiguity is expensive.
8. **Communicate clearly.**
Structure thoughts. Summarize complexity when discussions become messy.
### 🚗 Driving Lessons
1. **Do not ignore tire alignment and balancing.**
Small maintenance prevents larger issues.
2. **Yield when necessary.**
The road is not a battlefield.
3. **Assume unpredictability from others.**
Defensive driving saves lives.
4. **Focus.**
Distraction is costly.
5. **Drive carefully rather than rushing.**
Arriving safely is the real goal.
6. **Understand basic maintenance.**
AC, oil, battery, brake pads, suspension, tires. Small awareness reduces big costs.
## Closing
I am still practicing these lessons.
Some days I apply them well. Some days I don’t.
But at 27, this is the framework I try to live by.
================================================================================
---
title: A Practical Guide to Data Standards for Seamless Collaboration and Data Integrity
description:
date: 2025-09-18T00:00:00.000Z
tags: ['engineering']
id: data-standards-alignment
---
# A Practical Guide to Data Standards for Seamless Collaboration and Data Integrity
## Introduction
If you’ve ever worked on a product with multiple engineers across frontend and backend, you’ll know how quickly things can fall apart without shared rules for handling data. Small differences-like whether dates should be UTC or local, or if `null` and `[]` mean the same thing-can create bugs that are painful to debug but trivial to avoid. I’ve been in that situation many times, and over the years I’ve settled on a set of lightweight standards that keep things predictable. This post is about those standards: pragmatic, easy-to-adopt rules that have saved me (and my teams) countless hours.
---
## Why Data Standards Matter
- **Consistency:** Everyone interprets data the same way.
- **Interoperability:** Frontend and backend don’t need to guess what a field means.
- **Maintainability:** Less `if/else` spaghetti to handle edge cases.
- **Debugging:** Issues are easier to track when the data shape is predictable.
**Real-world example:**
- Date and Time: the backend sometimes stored dates in local time while the frontend assumed UTC. Users in different regions saw reports shift by a day.
- An endpoint returned `null` for items, which immediately broke the UI. After we introduced clear standards, these problems disappeared.
---
## Common Data Standards
### 1. Date and Time: The Timezone Trap
- Always store in **UTC** on the server.
- Use **ISO 8601 format** (`2025-09-18T23:59:59Z`).
- ⚠️ We can also store dates as Unix timestamps (milliseconds since epoch). They’re compact and avoid parsing issues across environments. ISO strings are easier to read and debug. Pick one format and apply it consistently.
- ⚠️ UTC works well for past events, but scheduled future events can get tricky with timezone rule changes (like daylight savings shifts).
Example: a meeting scheduled at “12:00 America/New_York” might drift if we only store UTC.
Better approach: store the scheduled local time (12:00), plus the timezone identifier (America/New_York) so client app can correctly render even if rules change.
See [Jon Skeet’s article](https://codeblog.jonskeet.uk/2019/03/27/storing-utc-is-not-a-silver-bullet/) for a deep dive on this problem.
- Let the **frontend localize** for display.
- For ranges, store **EOD (End of Day)** as `23:59:59` and define the expected timezone.
- For recurring events, store **timezone metadata** separately.
**Example JSON:**
```json
{
"start_date": "2025-09-01T00:00:00Z",
"end_date": "2025-09-30T23:59:59Z",
"timezone": "Asia/Jakarta"
}
```
**Backend (Node.js):**
```ts
const endDate = new Date("2025-09-30");
endDate.setUTCHours(23, 59, 59, 999);
res.json({ end_date: endDate.toISOString() });
```
**Frontend (React):**
```tsx
const displayDate = dayjs(new Date(end_date)).format("MMM d, yyyy");
```
---
### 2. Boolean: When `null` Breaks Logic
- Client: default to treat `null` as **false,** unless there are special cases or agreements.
- If true/false isn’t enough, use **enums** (e.g., `pending`, `approved`, `rejected`).
- Document when a flag means **state** vs. **capability**.
**Example JSON:**
```json
{
"is_active": false,
"is_admin": true,
"status": "approved"
}
```
**Backend (Express):**
```ts
const isVerified = user.isVerified ?? false;
res.json({ isVerified });
```
**Frontend:**
```tsx
if (isVerified) {
return ;
}
return ;
```
---
### 3. Arrays: Empty or Null? Pick One.
- Default to return `[]` instead of `null` for empty array. Either as API response standard or the frontend always anticipate by defining fallbacks.
- `[]` explicitly communicates "no items".
- null can mean “not loaded yet” or "something went wrong", which introduces ambiguity.
- Returning `[]` keeps frontend logic simpler (`items.map()` won’t break).
- Keep array item types consistent (IDs, objects, not both).
- Clarify whether **ordering** is backend or frontend responsibility.
**Example JSON:**
```json
{
"roles": ["admin", "editor"],
"items": []
}
```
**Backend (Node.js):**
```tsx
const items = (await db.getItems()) || [];
res.json({ items });
```
**Frontend (React):**
```tsx
const items = fetchedItems ?? [];
{
items.length > 0 ? (
items.map((item) => )
) : (
No items found
);
}
```
---
In TypeScript or modern JS, we can simplify null handling with operators like `??` or `?..` For example:
```ts
const length = (array ?? []).length;
const safeValue = user?.isVerified ?? false;
```
Reference:
- https://dorey.github.io/JavaScript-Equality-Table/
---
### 4. Numbers: Precision Matters
- Store numeric values as **numbers**, not strings.
- For money, use **integers in smallest units** (e.g., cents).
> Important: Avoid doing critical math in the browser - floating point quirks can cause rounding errors. Always let the backend be the source of truth for final financial calculations, and use the frontend mainly for display/formatting. If you must calculate in the browser, add reverse checks (e.g., verify `a * b / b === a`) to detect anomalies.
- For precision (e.g., FX rates), use **decimal strings**.
- Define min/max limits (e.g., percentages 0–100).
**Example JSON:**
```json
{
"price": 4999,
"exchange_rate": "1.2345",
"discount_percent": 15
}
```
**Backend:**
```ts
res.json({ priceInCents: 999, userId: "123e4567-e89b-12d3-a456-426614174000" });
```
**Frontend:**
```tsx
const price = nstr(priceInCents / 100, { maxDecimals: 2 }); // "9.99"
```
---
### 5. Strings: Keep Them Clean
- Trim whitespace before saving.
- Normalize casing where relevant (emails always lowercase).
> Trimming and lowercasing help, but watch out for homoglyphs and invisible Unicode characters (e.g., user@example.com vs uѕer@example.com with a Cyrillic “s”). For sensitive fields like emails and usernames, use String.prototype.normalize() or dedicated libraries to ensure consistency and security.
- Use enums if the value should be constrained.
**Example JSON:**
```json
{
"name": "Nathan",
"email": "nathan@example.com",
"status": "active"
}
```
**Backend:**
```tsx
const email = req.body.email.trim().toLowerCase();
await db.save({ email });
```
**Frontend:**
```tsx
```
---
### 6. IDs and References: Don’t Mix Types
- Use **UUIDs** or consistent numeric IDs.
- Don’t mix `"123"` and `123`.
- Clarify whether IDs are internal-only or public-facing.
- Prefer referencing by ID over embedding full objects unless necessary.
**Example JSON:**
```json
{
"user_id": "a12b3c4d-5678-90ef-1234-567890abcdef",
"order_id": 1024
}
```
**Backend:**
```ts
res.json({ user_id: uuidv4() });
```
**Frontend:**
```ts
fetch(`/api/users/${user_id}`)
.then((res) => res.json())
.then((user) => setUser(user));
```
---
### 7. Error Handling: Predictable Responses
- Standardize errors with:
- `code`: machine-readable
- `message`: human-readable
- `details`: optional per-field errors
- Align with proper **HTTP status codes**.
**Example JSON:**
```json
{
"code": "INVALID_INPUT",
"message": "Email is not valid",
"details": { "email": "Invalid format" }
}
```
**Backend (Express):**
```tsx
res.status(400).json({
code: "INVALID_INPUT",
message: "Email is not valid",
details: { email: "Invalid format" },
});
```
**Frontend:**
```tsx
if (error.code === "INVALID_INPUT") {
showToast(error.message);
}
```
---
## Putting It All Together
Adopting standards isn’t about adding complexities-it’s about saving time. A few practical tips:
1. **Document early.** Put conventions in README or API docs.
2. **Validate automatically.** Use Zod, Yup, or backend schema validators.
3. **Enforce in code.** Add type-checking and linter rules.
4. **Review often.** Check API responses during code reviews.
These standards are small investments that pay off in stability and predictability.
---
## Conclusion
Data standards aren’t glamorous, but they are the foundation of reliable systems. The goal isn’t to enforce arbitrary rules-it’s to remove ambiguity and make intent clear. An empty array says “no items,” while `null` might mean “not loaded yet.” UTC timestamps work well for past events, but for scheduled future events we may need to store local time together with the timezone to avoid surprises when daylight saving rules shift.
The important part is not picking the “perfect” rule-it’s picking a rule that fits the use case, documenting it, and applying it consistently across frontend and backend. That’s what reduces friction, avoids hidden bugs, and keeps team aligned.
Start small. Document the basics. Enforce them in code. Over time, these conventions will feel natural-and wonder how we ever worked without them.
================================================================================
---
title: How to Secure Your Firebase Project
description:
date: 2021-05-13T00:00:00.000Z
tags: ['security', 'firebase']
id: how-to-secure-firebase-project
---
# How to Secure Your Firebase Project
> disclaimer: This is not by any means to be the best practice guide of using firebase in every project. Every project has its own needs and specifications. This guide may not be suitable with your needs.
Do you develop apps using Firebase? If so, we should know that our firebase configs will be exposed to the client (especially for web clients). Then what's the deal? Many possibilities can be happened and prevented. So many articles already covers on how to secure our firebase project by using security rules, authentication check, etc. I usually define my security rules using a package named [`@jahed/firebase-rules`](https://www.npmjs.com/package/@jahed/firebase-rules). But how about preventing someone from making their own client and use our exposed firebase configs to do some shady stuffs towards our realtime database?
## Apply Restrictions to the API Key
Every firebase project is a GCP (Google Cloud Platform) project, so we can go to Google Cloud console to configure further our firebase project. We can restrict the API key even though it is exposed. Just follow these steps:
1. Go to [https://console.cloud.google.com/apis](https://console.cloud.google.com/apis),

2. Select the desired firebase project,
3. Go to `Credentials` menu,
4. Look for `API Keys` section, select the API key which has `...(auto created by Firebase)`,

5. You will be directed to a page called `Restrict and rename API key`, go to `Application restrictions` section, select `HTTP referrers (web sites)`, add your production web client domain in `Website restrictions` section. Don't add `localhost` if you intend to use the firebase project for production.
6. Hit the `save` button to apply changes.
Voila! Your API key already restricted!
## Use Different Firebase Project for Local Development
Now that we already apply restrictions to our API key, how about our local development (`localhost`)? Well, we can just create a new firebase project which will be used for our local development.
### Special Case: Develop Hybrid Mobile App Using Ionic
When developing hybrid app using ionic, chances are we gonna use the same firebase configs for Web and Android / iOS build (firebase web configs). Even though we made several firebase web configs, the API key will remain the same. If we apply restrictions to the API key to be only accessible for certain domain, then the API key won't be usable for the mobile build. Fortunately, there is a workaround for this. We can just create a new API key which don't have any domain restrictions just for our mobile build (the API key can't be easily accessed by the user anyway for Android / iOS build). So, we can use the same firebase configs for our web and mobile builds, but having different API key. Just go to https://console.cloud.google.com/apis/credentials (make sure you already select the corresponding project first), then just create a new API key which will be used for your mobile apps.
### References
- Defining Firebase security rules
- [https://www.npmjs.com/package/@jahed/firebase-rules](https://www.npmjs.com/package/@jahed/firebase-rules)
- Restrict API key
- [https://medium.com/@devesu/how-to-secure-your-firebase-project-even-when-your-api-key-is-publicly-available-a462a2a58843](https://medium.com/@devesu/how-to-secure-your-firebase-project-even-when-your-api-key-is-publicly-available-a462a2a58843)
================================================================================
---
title: 2020 Recap
description:
date: 2020-12-31T00:00:00.000Z
tags: ['recap']
id: 2020-recap
---
# 2020 Recap
I think most would say 2020 is a roller coaster year. Even though, 2020 is one of a hella ride and worth for me to be remembered. I've made a quick recap in **[2019](https://agustinusnathaniel.com/blog/2019-recap)** before. So let's review what I've been up to in 2020.
## What I Learned and Discovered Throughout the Year
### Instagram Filter Creation
I love to edit photos and playing around with colors. Then I learn how to make my own Instagram filter. Turned out facebook provides a tool dedicated for that called Spark AR Studio. You can make literally almost anything starting from a color filter, games, quizzes, anything you can think of when playing around with AR. I just want to edit colors so I just go with color filter. Making a color filter is quite simple, you only have to create a LUT, import it to your Spark AR projects as a layer, export it, and upload it to Spark AR and connect it with your instagram account. Check out some filter I made [here](https://www.instagram.com/agustinusnathaniel/).
### React Hooks and Context
React hooks been around but I've never learn or use it until I finally get my hands on it when building an app as part of my undergraduate thesis. I used to stick with Classful Components and use redux (just in desperate moments), but learning hooks and context helps me to switch fully to Functional Components.
### TailwindCSS
2019 was a year where I finally get my hands on React and started to re-learn GatsbyJS (I tried to learn Gatsby before touching React at 2018 and it was a disaster idea). It was a really fun journey. But there's something I've been wanting to try but never accomplished at 2019....TailwindCSS. So, at 2020 I finally get my hands on it and it was really different than other styling frameworks I've ever used before (Bootstrap, Bulma, SemanticUI). I use it to rebuild my first personal site (agustinusnathaniel.com). But around the end of 2020 I re-write it again using Chakra-UI.
### Svelte
I heard svelte often but never tried it until I stumble upon Rich Harris's video when he explain about reactivity. His explanation is really great and quite eye-opening for me who mainly develop using React. So I tried it, made some little projects with it and I like how straightforward it is. I'd recommend using svelte if you are looking for a powerful javascript libraries like react or vue (framework) but with an easier learning curve. I'd love to see Svelte's development over the next years. But not long after that I also got my hands on Next.js.
### Next.js
I learn Next.js and implement it in some of my projects. I used to avoid learning and using Next.js because I haven't understand SSR properly yet. I thought Next.js is only used for SSR projects. Then I found out Next.js support both static site generation and server side rendering since version 9.3 and I got my hands on it for the first time not so long after version 9.4 released. Since then, Next.js has been my go-to react framework for most of my projects.
### Chakra UI
I tried several design system or component libraries this year: Fluent UI, Carbon, Material UI, and Chakra UI. After trying out and play around with those design systems, I found out Chakra UI suits my needs the best. It's easy to be configured, easy to use, and it has color mode styling and management built in. After some time, I made a template with Next.js, Chakra UI, and TypeScript pre-configured and has been my go-to template to initiate most of my recent projects.
## Projects I Worked on in 2020
most of the projects are just a little side projects for me to try implement some simple ideas or learning something.
### 1. Personal Site Revamp
I re-write my personal site [agustinusnathaniel.com](https://agustinusnathaniel.com) using TailwindCSS and add blog section. At the end of the year I replace the TailwindCSS usage with Chakra UI.
### 2. Le-Cook
An [app](https://le-cook.sznm.dev) to find food recipe, powered by RecipePuppy API
### 3. Covid-19 Data
[Monitor Covid-19 statistics](https://covid19.sznm.dev), powered by @mathdroid's Covid-19 API, @ariya's Dekontaminasi API, and @Reynaldi531's api-covid19-indonesia v2. First developed using Gatsby, then re-wrote it with Next.js and Chakra UI.
### 4. Advice Generator
A random [advice generator](https://advicegen.sznm.dev) powered by Advice Slip JSON API, written using Svelte.
### 5. Insta Profile
A simple Svelte [app](https://instaprofile.sznm.dev) fetching data from Instagram.
### 6. sznm.dev
I make [another personal site](https://sznm.dev) of myself dedicated for dev content, built using Next.js, composed using Chakra UI.
### 7. KapturaLumina
Basic Photography Learning Mobile App with Gamification. Built using Ionic, React, and Firebase. Available as [PWA](https://kapturalumina.sznm.dev) and [android](https://play.google.com/store/apps/details?id=dev.sznm.kapturalumina) app. I built it as part of my undergraduate thesis.
### 8. nextarter-chakra
A [template](https://nextarter-chakra.sznm.dev/) I made to initialize Next.js projects with TypeScript and Chakra UI setup. Most of my following projects are initalized / generated using this template.
### 9. Add to Calendar Generator
A web [app](https://addtocal.sznm.dev) to generate Add to Calendar link (Google Calendar).
### 10. InstaDLD
Instagram post media [downloader](https://instadld.sznm.dev) with multipost download supported.
### 11. Public APIs
An [app](https://publicapis.sznm.dev) to find public API for you next projects. Didn't thought it could be the [product of the day](https://www.producthunt.com/posts/public-apis-3) in Product Hunt at 26 Dec 2020. Powered by api.publicapis.org.
## Some other stuffs worth to mention
- I graduated from college (finally) 😊😊. Really grateful for everyone who supported me until now, especially my family and friends.
- I started my career as Software Engineer right after I finished my thesis.
All in all, I'm glad I can get through 2020 pretty well enough. I never imagined I'd graduate in time, especially when the pandemic situation started to happen and my thesis proposals got rejected several times. Hoping to grow more in 2021 and crafting greater stuffs. Thank you for reading and I hope you are doing well 😄.
If you're interested in another version of my review of my journey in 2020, I also published it at [twitter](https://twitter.com/agstnsnathaniel/status/1345261139358142467) (focused around the projects I made throughout the year).
My previous recap: **[2019](https://agustinusnathaniel.com/blog/2019-recap)**.
================================================================================
---
title: Monitor and Measure Site Performance from Time to Time and Automatically using Speedlify
description:
date: 2020-11-07T00:00:00.000Z
tags: ['performance', 'monitoring']
id: monitor-and-measure-site-performance-with-speedlify
---
# Monitor and Measure Site Performance from Time to Time and Automatically using Speedlify
## TL;DR
Want to measure and monitor your site performance from time to time and automatically? Deploy [Speedlify](https://speedlify.dev) through [Netlify](https://www.netlify.com/) and use [Github Actions](https://github.com/features/actions) (cron schedule) or Zapier to automatically trigger build every desired time.
You can directly visit [this page](https://github.com/zachleat/speedlify/#deploy-to-netlify) if you know what to do next. But if you need some guidance, this article will help you step by step.
### What will be covered in this article?
- How to configure Speedlify
- How to deploy Speedlify
- Using Netlify build hooks and Github Actions to automatically trigger build Speedlify page every desired time.
### This article won't cover...
SEO or visitor related metrics like Google Analytics. The site performance mentioned in this article is [Lighthouse](https://developers.google.com/web/tools/lighthouse) based performance measurement.
---
## Measuring Site Performance
Nowadays there are so many ways to measure site performance beside running lighthouse in your local machine (Chrome Dev Tools). Even recently [Vercel](https://vercel.com), PaaS for frontend deployment released a feature called [Analytics](https://vercel.com/docs/analytics/overview.amp). This feature can show your [Next.js](https://nextjs.org/analytics) or [Gatsby](https://vercel.com/blog/gatsby-analytics) site performance automatically from time-to-time without having to configure anything (_almost zero-config_). But this feature is limited to be applicable for one project if your vercel account is a free version.
What if we have more than one project / site to be measured from time-to-time without spending extra cost? Well, this is where [Speedlify](https://speedlify.dev) comes in, a template for site performance monitor created by [Zach Leatherman](https://github.com/zachleat). Speedlify built using a static site generator framework called [11ty(eleventy)](https://www.11ty.dev/).
I've tried to deploy my own speedlify [here](https://audit.sznm.dev).
_fun fact: I found Speedlify when I was just randomly exploring 11ty docs page (not really important, just intermezzo 😄)_
Well, let's get our hands dirty!
## How?
### What is needed?
- a Github account
- a Netlify account
### This Guide was Written with Assumtions that You:
- know how to use basic Git commands
- familiar with Netlify
- familiar with Node.js environment (installed node and npm)
I suggest you to visit [Speedlify](https://speedlify.dev) to get some glimpse on what we will be using. There's a link to the source code repository on that page which shows you how to deploy your own Speedlify page. However, if you are having some difficulties, you can folow these steps:
### #1: Clone Speedlify Repo
Import speedlify repository to your github account ([https://github.com/new/import](https://github.com/new/import)),
input this URL: `https://github.com/zachleat/speedlify/`.
After the import process is done, clone your speedlify repo into your local machine or just run these command below in your local folder:
```git
git clone https://github.com/[YOUR_GITHUB_USERNAME]/speedlify/
```
```bash
cd speedlify
```
Then, run `npm install` or `npm i`.
### #2: Configure URLs
Open `_data/sites` folder. Every file you create here will represent a category. You can defined more than one URL for every category.
Just delete all default files in `_data/sites`. Create a file `[CATEGORY_NAME].js`. (change [CATEGORY_NAME] with your desired category name). You can create more than one category, but you must know some limitations here: [https://github.com/zachleat/speedlify/#known-limitations](https://github.com/zachleat/speedlify/#known-limitations)
```js
// _data/sites/[CATEGORY_NAME].js
module.exports = {
name: "Category Name", // optional, falls back to object key
description: "Category Description",
options: {
frequency: 60 * 23, // 23 hours
// Use "run" if the sites don’t share assets on the same origin
// and we can reset chrome with each run instead of
// each site in every run (it’s faster)
// Use "site" if sites are all on the same origin and share assets.
freshChrome: "run",
},
urls: [
"https://[YOUR_SITE_URL]/",
"https://[YOUR_SITE_URL]/",
// etc
],
};
```
#### explanations
- `options`
- `frequency`: to set minimum time needed before next measurement. If we set `60*23` (1380 minutes or 23 hours) it means we will be measuring our site performance once every 23 hours. This value will be used to avoid measurement more than once before the minimum time was passed which will affect the build time. If we set the frequency to 23 hours and trigger build every 6 hours, the measurement for this category will be skipped if the last measurement haven't passed 23 hours.
### #3: Test run in local
Run `npm run start`. If the categories you input are shown, you can continue to the next step. You won't see any measurements. Measurements will be done at the build time when we deploy your Speedlify to Netlify.
Commit your changes and run `git push` to apply changes to your github repository.
### #4: Deploy configured Speedlify through Netlify
Open your Netlify dashboard ([https://app.netlify.com/](https://app.netlify.com/)), click "New site from Git". Point it to your Speedlify repository.

Confirm the build configurations until "Deploy Site" button is shown and click that button. Netlify build will do the build and deployment process. If the build and deployment process are successful, you can preview your deployment.
The measurement page will look like this:

### #5: Configure Github Actions to Automatically Trigger Build Every Desired Time
To do measurement from time-to-time, we will utilize Netlify build hooks and Github Actions. Why? Because measurement are only done on build time. It will be a hassle to trigger the build process manually. You don't have to use Github Actions if you prefer to use Zapier or similar services which support cron schedule. The same process can be achieved using Zapier with Schedule by Zapier and Webhooks by Zapier. However for the time being Webhooks by Zapier can only be enabled if you are a Zapier premium user. Therefore, in this guide I will use Github Actions as an alternative which are more friendly to our pocket.
We will need build hook link (webhooks) to trigger build in Netlify. To get that link, open your speedlify project in your Netlify dashboard, then open "Site settings". Open "Build & Deploy".

Then point to "Build hooks", click "Add build hook".

We will get the build hooks URL, copy that link.

Now we can configure Github Actions to automatically trigger build to Netlify. Go back to your speedlify local folder, add a file named `.github/workflows/main.yml` and paste the copied build hooks URL into this file:
```yml
# .github/workflows/main.yml
# edit according to your needs
name: Trigger Netlify Build daily on Weekday
on:
schedule:
# if you want to define your own build trigger schedule, just change the cron schedule value below
# use https://crontab.guru/ if you are having some difficulties on how to define the cron values
- cron: "0 22 * * MON-FRI"
# every day on weekdays 22:00.
jobs:
build:
name: Netlify build
runs-on: ubuntu-latest
steps:
- name: Curl request
run: curl -X POST -d {} YOUR_BUILD_HOOK_URL
```
Commit your changes and run `git push`. If it's configured correctly, it will be shown in "Actions" tab at your github repository.
Now you have a dedicated page to monitor your site performance from time-to-time and automatically updated.
## Limitations
It's important to note that the more URL you add to your speedlify configurations, the build time needed will be increased. The free version of Netlify have a maximum 15 minutes build time for every build process and 300 minutes build time quota for every month.
I work around it by limiting my measurement to be done maximum once every 23 hours and limiting the total URLs from all category to be around 5 until 10, and automatically trigger the build every day on weekdays only at 10PM. With those configurations, every build would spend around 4 until 7 minutes (<15 minutes), so I won't hit the monthly build time quota limit (4.5 x 5 x 7 ~= 160 minutes -> <300 minutes).
## Thank You!
For reading this article. I hope you found this useful.
## References
- [https://www.speedlify.dev/](https://www.speedlify.dev/)
- [https://github.com/zachleat/speedlify/#deploy-to-netlify](https://github.com/zachleat/speedlify/#deploy-to-netlify)
- [https://github.com/zachleat/speedlify/#known-limitations](https://github.com/zachleat/speedlify/#known-limitations)
- [https://www.zachleat.com/web/speedlify/](https://www.zachleat.com/web/speedlify/)
## Some Alternatives
(some exhaustive list of other tools to measure your website performance)
- [https://web.dev/measure/](https://web.dev/measure/)
- [https://www.lightest.app/](https://web.dev/measure/)
================================================================================
---
title: May 2020 Quarantine Self Challenge
description:
date: 2020-05-02T00:00:00.000Z
tags: ['Gatsby', 'Vercel', 'Evergreen', 'TailwindCSS']
id: quarantine-self-challenge
---
# May 2020 Quarantine Self Challenge
Recently I challenged myself to make a web app as soon as possible in two days. Managed to make two. One is a simple Food Recipe App and the other is COVID-19 Data App.
Turned out it was so fun.
## 1. Le Cook
Fun food recipe catalog app powered by [RecipePuppy](https://recipepuppy.com) API....
[https://le-cook.now.sh/](https://le-cook.now.sh/)



## 2. COVID-19 Data
presenting COVID-19 statistics powered by [@mathdroid](https://github.com/mathdroid/covid-19-api/)'s covid-19-api.
I made a vanilla JS version of this app several days ago and I decided to re-develop it using Gatsby (React).
[https://covid19data.now.sh/](https://covid19data.now.sh/)



Both [Le Cook](https://le-cook.now.sh/) and [COVID-19 Data](https://covid19data.now.sh/) app were developed using Segment's Evergreen UI. Just discovered this React UI Framework and turns out it's so convenient to implement it on these projects.
When developing COVID-19 Data app, I realized Evergreen UI have no opinionated way to construct responsive layouts. So, I decided to combine it with TailwindCSS which I already tried to use at this personal site. It worked.




I decided to deploy it using [Vercel](https://vercel.com) and I'm so shocked by how fast it is compared to [Netlify](https://netlify.com). I'm considering to migrate to Vercel from Netlify for my previous projects, including this site.
I posted my recent projects [here](https://agustinusnathaniel.com/projects).
You can access the project and the project's GitHub repository link from this [page](https://agustinusnathaniel.com/projects).
================================================================================
---
title: Stackbit, the Game Changer
description: You can launch a powerful and beautiful static site in just under 10 minutes and at super low-cost.
date: 2020-03-22T00:00:00.000Z
tags: ['Stackbit', 'Netlify', 'Github', 'Sanity']
id: stackbit-the-game-changer
---
# Stackbit, the Game Changer
_by the time I wrote this post, Stackbit is still at Beta._
Have you ever thought of launching a landing page or a blog with the technology of your choice in just under 10 minutes? Since JAMStack is getting more popular these days, more sites are being developed this way. Building it from scratch is also not too hard. But the problem is sometimes the initialization of the project or the deployment process can spend more time than you think. Not to mention if you are someone who just want to get a little grasp of what all this about without getting your hands dirty and spending some hours just to know what are you actually trying to do. Or if you are a first-timer who needs a step-by-step guide.
Yes, this is for you if you want to get a taste of what is JAMStack site about, how does it work in general, but you don't want to build it from scratch just to know it. This is for you if you are someone who are very familiar with building JAMStack sites, having multiple and upcoming clients who requests high performance yet low-cost site. Well, this is where Stackbit plays the role for you.
You can call Stackbit as your smart-assistant who helps you to start your JAMStack site. It's very easy to use, you just need to prepare your GitHub account (GitLab and BitBucket support coming soon) and a Netlify account. The next thing is just register yourself into Stackbit (you can even register using your GitHub account so it's more convenient) and you can start to Build a Project. You gonna choose the site theme, the site generator framework, CMS, then you can deploy it.

All of it are done just by picking, doing some clicks, and voila. Yeah, all of the process involve no code. But if you want to develop and customize the project further, that's where your hands start getting dirty.
So, what are you waiting for? Don't just read this post, it won't bring you anywhere. _Try it now_ and you will know what I'm talking about 😂.
================================================================================
---
title: 2019 Recap
description:
date: 2019-12-31T00:00:00.000Z
tags: ['recap']
id: 2019-recap
---
# 2019 Recap
I spent most of my time in 2019 at college, trying to finish all my university courses by the end of 7th semester so I can focus on Thesis / Final Project at the final semester (8th semester). Planning to graduate in 2020. So...not really much happened in this year, but I learned so much in terms of mobile application and web development. How to build an Android App and got my hands on React framework for the first-time in this year.
## What I learned in 2019
- Java and XML - How to build Native Android apps using Android Studio.
- REST API - How to fetch data and display it with recycler view in Android app
- PHP - how to program a website with PHP, MVC.
- How to setup a website using database with PHP, laragon, and MySQL
- CodeIgniter - how to use CodeIgniter to build website.
- How to deploy PHP sites with its database to cPanel hosting.
- How to setup arcanist in Linux Ubuntu.
- How to use Git for managing personal and team projects.
- TypeScript - how to use it, what is the importance of strong typing, and how to compile it into JS
- React - how to build an app using React with CRA.
- GraphQL - what is it and how is it used
- Apollo - how to implement apollo as a client for GraphQL
- GatsbyJS - how to build a site with Gatsby
- Ionic - how to build hybrid apps and how does it different from native apps.
- Angular2+ - forced to learn this framework because learning Ionic Angular and luckily I've learned TypeScript.
- Zeit - found a Netlify alternative and how to deploy site with it.
================================================================================
================================================================================
TIL - TODAY I LEARNED (16)
================================================================================
---
title: Web Share API
date: 2025-10-01T00:00:00.000Z
tags: ['javascript', 'function']
id: navigator-share
---
# Web Share API
There's an interface to invoke native sharing mechanism of the device through Navigator: share() method. Most browsers and OS already support this method these days. Hence, this method is still limited in support (especially on Linux or some other OS).
```ts
const shareData = {
title: "Hello",
url: "https://your-url.com",
};
await navigator.share(shareData);
```
References:
- https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
- https://developer.mozilla.org/en-US/docs/Web/API/Navigator/canShare
- https://wpt.live/web-share/
================================================================================
---
title: JavaScript: Exponentiation
date: 2025-01-24T00:00:00.000Z
tags: ['javascript', 'mathematic']
id: js-exponentiation
---
# JavaScript: Exponentiation
Exponentiation operator (\*\*) raises the first operand to the power of the second operand. Equivalent to `Math.pow()`
```js
// Example
console.log(3 ** 4); // 81
console.log(5 ** 3); // 125
console.log(10 ** -2); // 0.01
console.log((3 ** 2) ** 3); // 729
```
References: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
================================================================================
---
title: vite-plugin-validate-env
date: 2025-01-03T00:00:00.000Z
tags: ['vite', 'typescript', 'zod']
id: vite-plugin-validate-env
---
# vite-plugin-validate-env
I sometimes put stringified JSON and has bunch of env variable keys. It's hard to know when there's an issue within the env value unless we really understand the whole env vars we define and where its used beneath the app.
Astro v5 has env validator built in. `t3-env` lib also serves this purpose. It's also possible to write custom validator and run it as custom script.
I look for seamless validation on my vite app and glad that there's a vite plugin for it: [@julr/vite-plugin-validate-env](https://www.npmjs.com/package/@julr/vite-plugin-validate-env). It runs on dev & build time only and support zod.
================================================================================
---
title: Biome Daemon Logs
date: 2024-09-16T00:00:00.000Z
tags: ['biome', 'cache', 'storage']
id: biome-daemon-logs
---
# Biome Daemon Logs
We can remove the logs using the clean command:
```bash
biome clean
```
References:
- https://biomejs.dev/guides/integrate-in-editor/#daemon-logs
- https://x.com/birch_js/status/1835305326083772641
================================================================================
---
title: VSCode Custom Label
date: 2024-05-01T00:00:00.000Z
tags: ['vscode']
id: vscode-custom-labels
---
# VSCode Custom Label
[More info](/notes/vscode-custom-labels)
References:
- https://x.com/nextjs/status/1783508313113800930
- https://code.visualstudio.com/docs/getstarted/userinterface#_customize-tab-labels
- https://code.visualstudio.com/updates/v1_88#_custom-labels-for-open-editors
================================================================================
---
title: Expo Env Load Behavior
date: 2024-04-30T00:00:00.000Z
tags: ['mobile', 'react-native', 'expo']
id: expo-env-load-behavior
---
# Expo Env Load Behavior
Expo has different behavior on how it loads env variables on dev server, EAS Build, and EAS Updates
[More info](/notes/expo-env-load-behavior)
================================================================================
---
title: React isValidElement
date: 2024-02-06T00:00:00.000Z
tags: ['react']
id: react-is-valid-element
---
# React isValidElement
```ts
type SomeComponentProps = {
description: React.ReactNode;
}
const ComponentProps = ({ description }: SomeComponentProps) => {
if (React.isValidElement(description)) {
return description;
}
return ({description});
}
```
References: https://react.dev/reference/react/isValidElement
================================================================================
---
title: TypeScript: Function Overloads
date: 2023-11-15T00:00:00.000Z
tags: ['typescript', 'function']
id: ts-function-overloads
---
# TypeScript: Function Overloads
We can define multiple function declarations to define different signatures (parameters/return types) and TypeScript will infer the right signature based on call arguments.
Some gotchas:
- Implementation should narrow types based on overload
- Order matters: put most specific overloads first
Use Cases:
- Reusable functions with different types
- Library functions mimicking overloads
- Gradual typing - multiple signatures
Example:
```ts
// Overloads
function getString(opts: { fallback: string }): string;
function getString(opts: {}): string | undefined;
// Implementation
function getString(opts: {}): string | undefined {
// ...
}
// Usage
const str1 = getString({ fallback: "hello" }); // string
const str2 = getString({}); // string | undefined
```
References: [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads)
================================================================================
---
title: PNPM Store Prune
date: 2023-10-25T00:00:00.000Z
tags: ['pnpm', 'storage']
id: pnpm-store-prune
---
# PNPM Store Prune
There's a way to easily clean up pnpm store, especially for unreferenced packages.
```bash
pnpm store prune
```
References: https://pnpm.io/cli/store#prune
================================================================================
---
title: vite-tsconfig-paths
date: 2023-10-11T00:00:00.000Z
tags: ['vite', 'typescript']
id: vite-tsconfig-paths
---
# vite-tsconfig-paths
I used to manually define how to resolve import through vite config `resolve` even though I already define path mapping in `tsconfig.json`. By using this plugin, it will read the tsconfig path mapping.
References: https://www.npmjs.com/package/vite-tsconfig-paths
================================================================================
---
title: vite-plugin-checker
date: 2023-09-05T00:00:00.000Z
tags: ['vite', 'typescript', 'eslint']
id: vite-plugin-checker
---
# vite-plugin-checker

I was looking for how to improve my vite react project by showing type-check or lint error like [CRA](https://create-react-app.dev)'s error overlay. Then I found a plugin called vite-plugin-checker. The configuration is pretty straightforward.
References: https://vite-plugin-checker.netlify.app/
================================================================================
---
title: Zod refine or superRefine check
date: 2023-07-28T00:00:00.000Z
tags: ['zod', 'typescript']
id: zod-refine-after-invalid-type-pass
---
# Zod refine or superRefine check
I sometimes got stuck when doing validations through `refine` or `superRefine`. Turns out `refine` and `superRefine` checking is run after there's no more invalid type error on the base schema. It's kinda expected but idk why is this not documented well.
================================================================================
---
title: FlutterShark
date: 2023-02-01T00:00:00.000Z
tags: ['mobile', 'flutter']
id: flutter-shark-app
---
# FlutterShark
[Android OS] [FlutterShark](https://play.google.com/store/apps/details?id=com.fluttershark.fluttersharkapp) able to detect and list down which app being installed in device are built using flutter. It will give these informations:
- Flutter SDK version
- Dart SDK version
- Packages being used
================================================================================
---
title: JavaScript: structuredClone
date: 2023-01-19T00:00:00.000Z
tags: ['javascript', 'function']
id: js-structured-clone
---
# JavaScript: structuredClone
JavaScript has built-in function called [sctructuredClone](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) which able to deep clone objects.
References: https://www.builder.io/blog/structured-clone
================================================================================
---
title: Flutter: Two Finger Scroll
date: 2022-04-01T00:00:00.000Z
tags: ['mobile', 'flutter']
id: flutter-two-finger-scroll
---
# Flutter: Two Finger Scroll
One of the quirks that remain apparent in apps built using flutter is when you scroll inside the app using two fingers. It will scrolls twice as fast.
> References: https://github.com/flutter/flutter/issues/11884
================================================================================
---
title: Flutter: Scrolling Screenshot
date: 2021-12-28T00:00:00.000Z
tags: ['mobile', 'flutter']
id: flutter-scrolling-screenshot
---
# Flutter: Scrolling Screenshot
Try to take screenshot inside the app when opening a long vertical pages (list pages etc). If there are no scroll option when taking the screenshot, highly possible its built using flutter.
https://twitter.com/agstnsnathaniel/status/1475856375863398402
================================================================================
================================================================================
NOTES (21)
================================================================================
---
title: Export Dataset to Excel
description: Export dataset to excel file
date: 2025-08-11T00:00:00.000Z
tags: ['utils']
id: export-dataset-to-excel
---
# Export Dataset to Excel
```ts
type HandleExportDataParams = {
dataSource: Array | undefined;
tableMapper?: (rowData: TData) => Record;
filename?: string;
};
const handleExportData = async ({
dataSource,
tableMapper,
filename,
}: HandleExportDataParams) => {
const data = [...(dataSource ?? [])];
const mappedData = tableMapper ? data.map(tableMapper) : data;
const worksheet = utils.json_to_sheet(mappedData);
const workbook = utils.book_new();
utils.book_append_sheet(workbook, worksheet, "Export");
writeFile(workbook, ensureXlsxExtension(filename ?? "export.xlsx"), {
compression: true,
});
};
function ensureXlsxExtension(input: string): string {
if (!input) return ".xlsx";
return input.endsWith(".xlsx") ? input : input.replace(/\.[^/.]*$/, "") + ".xlsx";
}
```
References:
- https://docs.sheetjs.com/docs/getting-started/installation/frameworks
- https://docs.sheetjs.com/docs/getting-started/examples/export#export-a-file
================================================================================
---
title: Cache Cleaning
description: Some cache I usually clean regularly
date: 2024-09-16T00:00:00.000Z
tags: ['pnpm', 'yarn', 'package-manager', 'biome', 'storage', 'cache']
id: cache-clean
---
# Cache Cleaning
# PNPM
```bash
pnpm store prune
```
References: https://pnpm.io/cli/store#prune
# Yarn
```bash
yarn cache clean
```
References: https://yarnpkg.com/cli/cache/clean
# Biome
```bash
biome clean
```
References: https://biomejs.dev/guides/integrate-in-editor/#daemon-logs
================================================================================
---
title: Biome Config
description: My personal biome configuration
date: 2024-09-10T00:00:00.000Z
tags: ['config', 'biome']
id: biome-config
---
# Biome Config
## v2
```json
{
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": ["src/**/*", "!turbo", "*.config.ts"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useSemanticElements": "error"
},
"complexity": {
"noExcessiveCognitiveComplexity": "error",
"noUselessStringConcat": "error",
"noUselessUndefinedInitialization": "error",
"useSimplifiedLogicExpression": "error",
"noVoid": "warn"
},
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error",
"noUnusedFunctionParameters": "error",
"useHookAtTopLevel": "error"
},
"performance": {
"noBarrelFile": "error",
"useTopLevelRegex": "error"
},
"style": {
"noDefaultExport": "error",
"useBlockStatements": "error",
"useCollapsedElseIf": "error",
"useDefaultSwitchClause": "error",
"useConsistentArrayType": {
"level": "error",
"options": {
"syntax": "generic"
}
},
"useFilenamingConvention": {
"level": "error",
"options": {
"filenameCases": ["kebab-case"]
}
}
},
"suspicious": {
"noDuplicateElseIf": "error",
"noConsole": {
"level": "error",
"options": {
"allow": ["error", "info"]
}
},
"noEmptyBlockStatements": "error",
"useAwait": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"overrides": [
{
"includes": ["src/lib/pages/**/index.tsx", "src/*.ts", "*.ts"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
}
}
}
},
{
"includes": ["src/routes/**/*"],
"linter": {
"rules": {
"style": {
"useFilenamingConvention": "off"
}
}
}
}
],
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
[":URL:", ":NODE:", ":PACKAGE:"],
":BLANK_LINE:",
[":ALIAS:"],
":BLANK_LINE:",
[":PATH:"]
]
}
}
}
}
}
}
```
---
## v1
```json
// biome.json (v1)
{
"$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useSemanticElements": "error"
},
"complexity": {
"noExcessiveCognitiveComplexity": "error",
"noUselessStringConcat": "error",
"noUselessUndefinedInitialization": "error",
"useSimplifiedLogicExpression": "error",
"noVoid": "warn"
},
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error",
"noUnusedFunctionParameters": "error",
"useHookAtTopLevel": "error"
},
"nursery": {
"noDuplicateElseIf": "error"
},
"performance": {
"noBarrelFile": "error",
"useTopLevelRegex": "error"
},
"style": {
"noDefaultExport": "error",
"useBlockStatements": "error",
"useCollapsedElseIf": "error",
"useDefaultSwitchClause": "error",
"useConsistentArrayType": {
"level": "error",
"options": {
"syntax": "generic"
}
},
"useFilenamingConvention": {
"level": "error",
"options": {
"filenameCases": ["kebab-case"]
}
}
},
"suspicious": {
"noConsole": "error",
"noConsoleLog": "error",
"noEmptyBlockStatements": "error",
"useAwait": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"overrides": [
{
"includes": ["src/lib/pages/**/index.tsx", "src/*.ts", "*.ts"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
}
}
}
},
{
"includes": ["src/routes/**/*"],
"linter": {
"rules": {
"style": {
"useFilenamingConvention": "off"
}
}
}
}
]
}
```
================================================================================
---
title: Open WhatsApp Link
description: Utility function to open WhatsApp toward specific number and pre-defined message
date: 2024-07-22T00:00:00.000Z
tags: ['react', 'react-native']
id: open-whatsapp-utils
---
# Open WhatsApp Link
```ts
const whatsappApiOpenContactUrl = "https://wa.me";
type OpenWhatsAppParams = {
phoneNumber: string;
regionCode?: string;
message?: string;
};
export const openWhatsapp = ({ phoneNumber, regionCode = "ID", message }: OpenWhatsAppParams) => {
const parsedPhoneNumber = parsePhoneNumber(phoneNumber, { regionCode });
const destinedPhoneNumber =
parsedPhoneNumber.number?.international.replace(/[+\s-]/g, "") ?? phoneNumber;
const url = new URL(`${whatsappApiOpenContactUrl}/${destinedPhoneNumber}`);
if (message) {
url.searchParams.set("text", message);
}
Linking.openURL(url.toString()); // react native
// window.open(url.toString(), "_blank"); // web
};
```
================================================================================
---
title: Force Update - Compare Version
description: compare version to check should force update or not
date: 2024-05-31T00:00:00.000Z
tags: ['javascript', 'typescript', 'app']
id: compare-version-force-update
---
# Force Update - Compare Version
A utility to check whether we should prompt force update or not
```ts
type ShouldForceUpdateParams = {
currentVersion: string;
minVersion: string;
};
const shouldForceUpdate = ({ currentVersion, minVersion }: ShouldForceUpdateParams) => {
return (
minVersion.localeCompare(currentVersion, undefined, {
numeric: true,
sensitivity: "case",
}) > 0
);
};
```
================================================================================
---
title: VSCode Custom Label
description: customize display label for editor tabs and even quick search
date: 2024-05-01T00:00:00.000Z
tags: ['vscode']
id: vscode-custom-labels
---
# VSCode Custom Label
Example:
```json
// .vscode/settings.json
{
"workbench.editor.customLabels.patterns": {
"**/lib/**/index.{ts,tsx}": "${dirname}",
"**/lib/views/**/index.{ts,tsx}": "${dirname} - Page",
"**/lib/components/**/index.{ts,tsx}": "${dirname} - Component"
}
}
```
References:
- https://x.com/nextjs/status/1783508313113800930
- https://code.visualstudio.com/docs/getstarted/userinterface#_customize-tab-labels
- https://code.visualstudio.com/updates/v1_88#_custom-labels-for-open-editors
================================================================================
---
title: Expo Env Load Behavior
description: behavior on how Expo load env on dev server, EAS Build, and EAS Updates
date: 2024-04-30T00:00:00.000Z
tags: ['mobile', 'react-native', 'expo']
id: expo-env-load-behavior
---
# Expo Env Load Behavior
Expo has different behavior on how it loads env variables on dev server, EAS Build, and EAS Updates
- On dev server and EAS Updates, by default it will load `.env`
- On EAS Build (unless we upload `.env` to EAS Build or exclude .env\* from .gitignore), it's recommended to define it in `eas.json`. EAS build only has access to files being not ignored through gitignore and EAS secrets.
- To make sure EAS Updates load the desired env:
1. Define `NODE_ENV` and `.env.${ENVIRONMENT_NAME}`. For example we want to use `.env.production`:
2. Add `--clear-cache` to make sure it load the most fresh value
```bash
npx cross-env NODE_ENV=production eas update --channel production --message "fix issues"
```
- references: https://github.com/expo/eas-cli/issues/2174#issuecomment-2053109651
References:
- https://docs.expo.dev/guides/environment-variables/
- https://docs.expo.dev/build-reference/variables/#can-eas-build-use-env-files
- https://docs.expo.dev/eas-update/environment-variables/#using-env-files-with-eas-update
- https://docs.expo.dev/build-reference/android-builds/
- https://docs.expo.dev/build-reference/ios-builds/
================================================================================
---
title: Gitconfig Setup
description: How to Setup Git for Multiple Account in Local Machine
date: 2023-10-28T00:00:00.000Z
tags: ['git']
id: gitconfig-setup
---
# Gitconfig Setup
It's common to have different / multiple Git accounts (personal Github account and work git account). Sometimes it could be a problem if committing changes to work repo using personal git account. It would be a hassle to keep switching git account each time we wanna commit. There's a more convenient way to automatically commit with the right git account according to the folder / repo being worked on. Enter `gitconfig` ✨.
Follow these steps to setup multiple gitconfig in local machine:
## Generate SSH Keys for Each Git Account
SSH keys are used for authentication, and different keys are required for different accounts. You can generate SSH keys using the command `ssh-keygen` in the terminal. Make sure you generate different SSH keys for each Git account.
## Add SSH Keys to Git Accounts
Once you have generated SSH keys, you need to add them to your Git accounts. You can add SSH keys by going to the settings of your Git account and adding the public key.
## Configure GitConfig or SSH Config
There are two ways to achieve resolving correct SSH key for each git account. The first one is folder base (with GitConfig) and the other one is host match (using SSH Config). Use whichever suits to your need.
### Option 1: GitConfig Setup
```gitconfig
// .gitconfig
[user]
email = your_email@email.com
name = your_name
username = your_username
[includeif "gitdir:example/work/folder/"]
path = ~/.gitconfig_work
[credential]
helper = manager
```
```gitconfig
// .gitconfig_work
[user]
name = your_name
email = your_work_mail@email.com
username = your_work_username
[core]
sshCommand = "ssh -i ~/.ssh/your_work_git_ssh_key_file"
```
This configuration will match the current working repository folder location.
### Option 2: SSH Config Setup
You need to configure the SSH config file to use the correct SSH key for each Git account. You can edit the SSH config file by running the command `nano ~/.ssh/config` in the terminal. Add the following lines to the config file:
```config
# First account
Host github.com-first
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_first
# Second account
Host github.com-second
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_second
```
You can also use folder matching for the configuration file. For example, if you have multiple Git accounts for work, you can use the following configuration:
```config
# Work account
Host work.github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_work
```
This configuration will match any host that starts with `work.github.com`, allowing you to use the same SSH key for all of your Git accounts for work.
## Clone Git Repositories
You can now clone Git repositories using the SSH URLs for each account. For example, if you want to clone a repository from the first account, you can use the SSH URL `git@github.com-first:username/repo.git`.
References:
- https://git-scm.com/docs/git-config#EXAMPLES
- https://www.atlassian.com/git/tutorials/setting-up-a-repository/git-config
================================================================================
---
title: Force Absolute Path Import
description: How to enforce absolute path import
date: 2023-10-16T00:00:00.000Z
tags: ['import', 'eslint']
id: force-absolute-path-imports
---
# Force Absolute Path Import
We can configure our projects to use absolute path import. Hence, defining it only enables the absolute path import. It doesn't automatically enforce us to use absolute path import. Relative import will still works. How to make sure everyone working on the same project adheres to always use absolute path import? There must be a way to automate this checking and fix it.
Luckily, there's already an eslint plugin for that: [`eslint-plugin-no-relative-import-paths`](https://www.npmjs.com/package/eslint-plugin-no-relative-import-paths) by [MelvinVermeer](https://github.com/MelvinVermeer).
Install the plugin package:
```bash
pnpm i -D eslint-plugin-no-relative-import-paths
```
Add it to eslint config:
```js
// .eslintrc.js
/** @type {import('eslint').Linter.Config} */
module.exports = {
plugins: ['no-relative-import-paths'], // add to plugins
...,
rules: {
...,
'no-relative-import-paths/no-relative-import-paths': [
'warn',
{
allowSameFolder: true,
rootDir: 'src',
prefix: '@' // this might be different depending on your tsconfig / jsconfig configuration
},
],
},
...,
}
```
Related:
- [Jest Absolute Path Import](/notes/jest-path-mapping)
================================================================================
---
title: Jest Path Mapping
description: How to configure jest so it able to transform the mapping from tsconfig
date: 2023-10-16T00:00:00.000Z
tags: ['jest', 'typescript']
id: jest-path-mapping
---
# Jest Path Mapping
To enable absolute path import in our test files, we need to configure it first. `ts-jest` provides a helper to transform the mapping from `tsconfig`.
```js
// jest.config.js
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
// [...]
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: "/src", // this might depend on your tsconfig configuration
}),
};
```
References / Further Guide: https://kulshekhar.github.io/ts-jest/docs/getting-started/paths-mapping
================================================================================
---
title: Deployment or Production Build Error
description: some collective list of things to check when encountering deployment or production build error
date: 2023-08-24T00:00:00.000Z
tags: ['deployment', 'production']
id: deployment-error
---
# Deployment or Production Build Error
Some things to check
- Network Connection
- API Keys
- Allowed Origins
- DNS configuration
- **ENV variables**
- GitLab CI/CD Variables
- Container Management Platform
- PaaS Platform
- Feature Flags
- Missing variables
- Code Error
- False Branch
- Remote Config
- VPN
- Outstanding Bills
- False / Incorrect ENV values
- Reinstall node_modules
- Rebuild .next
- Compare package.json, make sure all required packages is installed
================================================================================
---
title: Fonts
description: Some of my favorite or frequently used fonts
date: 2023-08-23T00:00:00.000Z
tags: ['fonts', 'design', 'typography']
id: fonts
---
# Fonts
Mostly Sans Serif and Variable Fonts.
- [Outfit](https://fonts.google.com/specimen/Outfit)
- [Recursive](https://fonts.google.com/specimen/Recursive)
- [Nunito](https://fonts.google.com/specimen/Nunito)
- [Jost](https://fonts.google.com/specimen/Jost)
- [Plus Jakarta Sans](https://fonts.google.com/specimen/Plus+Jakarta+Sans)
- [Geologica](https://fonts.google.com/specimen/Geologica)
- [Figtree](https://fonts.google.com/specimen/Figtree)
- [Hanken Grotesk](https://fonts.google.com/specimen/Hanken+Grotesk)
- [Alexandria](https://fonts.google.com/specimen/Alexandria?preview.text=The%20quick%20brown%20fox%20jump%20over%20the%20window&preview.text_type=custom)
- [Lexend](https://fonts.google.com/specimen/Lexend)
- [Open Sans](https://fonts.google.com/specimen/Open+Sans)
- [Atkinson Hyperlegible](https://fonts.google.com/specimen/Atkinson+Hyperlegible)
- [Instrument Serif](https://fonts.google.com/specimen/Instrument+Serif)
- [Gantari](https://fonts.google.com/specimen/Gantari)
- [Noto Sans](https://fonts.google.com/specimen/Noto+Sans)
- [Poppins](https://fonts.google.com/specimen/Poppins)
================================================================================
---
title: Migrate to PNPM
description: Guide on how to migrate to pnpm easily
date: 2023-08-22T00:00:00.000Z
tags: ['pnpm']
id: migrate-to-pnpm
---
# Migrate to PNPM
- It's recommended to use corepack, add this to `package.json`
```json
...,
"engines": {
"node": ">=20.x",
"pnpm": ">=8"
}
```
- Run `pnpm import`
- Remove `package-lock.json` or `yarn.lock` file
- (If you have pre-install or post-install script) Add `.npmrc` containing:
```
enable-pre-post-scripts=true
```
- Add `.pnpm-debug.log*` to `.gitignore` file
- Replace `npm run` or `yarn` command with `pnpm`
================================================================================
---
title: Desktop Apps or Tools I Use
description: some apps / tools I usually use
date: 2023-08-18T00:00:00.000Z
tags: ['apps', 'tools', 'mac', 'windows']
id: desktop-apps-tools
---
# Desktop Apps or Tools I Use
## Mac and Windows
- [**VS Code**](https://code.visualstudio.com/): My favorite Code Editor
- [**HandBrake**](https://handbrake.fr/): Open source video transcoder, mainly use it for converting and/or compressing video files
- [**scrcpy**](https://github.com/Genymobile/scrcpy): Enables us to screencast our android devices to the computer and control the device directly. Just connect through USB / TCP over IP.
- [**Termius**](https://www.termius.com/): SSH client and terminal
## Mac Only
- [**Warp**](https://sznm.link/warp-terminal): Rust based smart terminal
- [**Rectangle**](https://rectangleapp.com/): for someone who used to Windows's window snapping behavior, this one is for similar experience in Mac.
- [**KeyCastr**](https://github.com/keycastr/keycastr): open source keystroke visualizer.
- [**MonitorControl**](https://monitorcontrol.app/): control external display brightness and volume easily.
- [**App Cleaner**](https://freemacsoft.net/appcleaner/): mac uninstall tool, just drag the app to be uninstalled and it will find related files to the app that also could be deleted.
- [**Coconut Battery**](https://www.coconut-flavour.com/coconutbattery/): monitor mac battery health.
- [**Zed**](https://zed.dev/): performant code editor by Atom creators.
## Windows Only
- [**ShareX**](https://getsharex.com/): all in one screen capture, productivity, etc tool
================================================================================
---
title: Force Lodash Import Scope
description: How to enforce lodash import scope
date: 2023-06-30T00:00:00.000Z
tags: ['lodash', 'eslint']
id: force-lodash-modules-import
---
# Force Lodash Import Scope
## Importing Lodash
There are multiple ways to import lodash utilities.
```ts
// whole // _.debounce();
// curly bracket / named // one-by-one / modules / single method ```
One of the recommended way to import it is `one-by-one` or modules or `single method` import as it will produces smallest bundle size.
## Enforce Lodash Import Method
How about if our project is:
1. maintained by multiple people, or
2. we have various parts in our code which utilize lodash
and we want to enforce specific way to import lodash methods?
We can use eslint and there's an eslint plugin for it: [`eslint-plugin-lodash`](https://www.npmjs.com/package/eslint-plugin-lodash). This plugin has a rule named `import-scope`. Here's how to configure it:
```bash
pnpm i -D eslint eslint-plugin-lodash
```
```js
// .eslintrc.js
/** @type {import('eslint').Linter.Config} */
module.exports = {
plugins: ["lodash"],
rules: {
"lodash/import-scope": [
"error",
"method" /** 'method' | 'member' | 'full' | 'method-package' */,
],
},
};
```
With this configuration, eslint will warns us when we import lodash methods using other than the preferred import scope.
### References:
- https://www.blazemeter.com/blog/import-lodash-libraries
- https://github.com/wix-incubator/eslint-plugin-lodash/blob/master/docs/rules/import-scope.md
================================================================================
---
title: Show Installed Global Node Deps
description: How to show installed global npm / yarn / pnpm dependencies
date: 2023-02-10T00:00:00.000Z
tags: ['node', 'pnpm', 'npm', 'yarn']
id: show-installed-global-node-deps
---
# Show Installed Global Node Deps
To check node dependencies which are installed globally.
## pnpm
```bash
pnpm list -g --depth 0
```
references:
- https://pnpm.io/pnpm-cli
- https://pnpm.io/cli/list#--global--g
## npm
```bash
npm list -g --depth=0
```
references: https://docs.npmjs.com/cli/v9/commands/npm-ls#global
## Yarn
```bash
yarn global list
```
references: https://classic.yarnpkg.com/en/docs/cli/global
================================================================================
---
title: Data Fetching
description: Fetch data with ease
date: 2022-10-24T00:00:00.000Z
tags: ['http-client', 'data-fetching', 'swr', 'react']
id: data-fetching
---
# Data Fetching
## Fetcher Utility
> install [axios](https://axios-http.com/)
```bash
pnpm add axios
```
> add fetcher utility for GET
```ts
type FetcherArgs = {
url: string;
params?: any;
};
export const fetcher = ({ url, params }: FetcherArgs) =>
axios.get(url, { params }).then((res) => res.data);
```
## Data Hook with SWR
> install [SWR](https://swr.vercel.app)
```bash
pnpm add swr
```
> add swr hook utility
```ts
type UseAppSWRArgs = {
url: string;
params?: any;
fallbackData?: ResType;
isReady?: boolean;
};
export const useAppSWR = ({
url,
params,
fallbackData,
isReady = true,
}: UseAppSWRArgs) => {
const { data, error, mutate } = useSWR(
isReady ? { url, params } : null,
fetcher,
{
fallbackData,
},
);
return {
data,
isLoading: !error && !data && isReady,
isError: error,
mutate,
};
};
```
> add data hook
```ts
// useMovieData.ts
export const useMovieData = (params?: any) =>
useAppSWR({
url: "https://some-api-url.com/api/movies",
params,
});
```
> call the data hook in the component
```tsx
// SomeComponent.tsx
const SomeComponent = () => {
const { data, isLoading } = useMovieData();
if (isLoading) {
return Loading...
;
}
return (
{data.results.map((item) => (
{item.title}
))}
);
};
```
================================================================================
---
title: App Folder Structure
description: How I usually structure my app folder
date: 2022-10-22T00:00:00.000Z
tags: ['app', 'foldering', 'structure']
id: app-folder-structure
---
# App Folder Structure
## Folder Structure
```bash
- src
- app/pages/routes
- lib
- components
- ui/shared
- [domain/feature]
- constants
- hooks
- layouts
- services/repository
- stores
- styles
- types/models
- utils
```
### lib
`lib` is where all the application building block lives. I like to separate building blocks from framework specific folders (app / pages).
#### components
`components` is where the UI building block lives. Sometimes I separate it further into shared and scope specific.
#### constants
all reusable or root level constants are maintained through this folder.
#### hooks
`hooks` is where I put reusable or page specific hooks. Sometimes I put it inside `hooks` folder, sometimes I put it under `components` if the hook is component specific.
#### layouts
where I put the app root or reusable layouts / page wrappers.
#### services
where I put services code (data fetching, API / third party integrations, etc). Sometimes I name the folder `repository`
#### stores
where I maintain global stores
#### styles
where I put root / global level styling / theme files.
#### types
where I maintain global / shared typings / schemas used across the app. Sometimes I name this folder `models`
#### utils
where I put all reusable utilities.
================================================================================
---
title: Custom Scrollbar
description: define your own custom scroll bar
date: 2022-07-22T00:00:00.000Z
tags: ['css', 'chakra-ui']
id: custom-scroll-bar
---
# Custom Scrollbar
## CSS
```css
::-webkit-scrollbar {
width: 0.75rem;
height: 0.75rem;
background-color: blue;
}
::-webkit-scrollbar-thumb {
border-radius: 20px;
background-color: gray;
}
/** firefox **/
html {
scrollbar-width: thin;
scrollbar-color: blue;
}
```
## References
- MDN
- [https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar#browser_compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar#browser_compatibility)
- [https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scrollbars#browser_compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scrollbars#browser_compatibility)
- W3Schools: [https://www.w3schools.com/howto/howto_css_custom_scrollbar.asp](https://www.w3schools.com/howto/howto_css_custom_scrollbar.asp)
- [agustinusnathaniel.com](https://agustinusnathaniel.com) - chakra-ui implementation
- [https://github.com/agustinusnathaniel/agustinusnathaniel.com/commit/f967221e40c7d680eb25ca4944ede4f5def2b628](https://github.com/agustinusnathaniel/agustinusnathaniel.com/commit/f967221e40c7d680eb25ca4944ede4f5def2b628)
- [https://github.com/agustinusnathaniel/agustinusnathaniel.com/commit/76c3ce6895b6de5b0a0d4195378cf68cde269054](https://github.com/agustinusnathaniel/agustinusnathaniel.com/commit/76c3ce6895b6de5b0a0d4195378cf68cde269054)
================================================================================
---
title: Overflow Scroll without Scrollbar
description: for sleek overflow scroll in mobile viewport
date: 2022-07-22T00:00:00.000Z
tags: ['css', 'chakra-ui']
id: overflow-scroll-without-scrollbar
---
# Overflow Scroll without Scrollbar
## CSS
```css
.some-component {
overflow-x: scroll; /* or overflow-y */
}
.some-component::-webkit-scrollbar {
display: none;
}
```
## Chakra-UI
```jsx
...some children
```
## References
- [https://stackoverflow.com/questions/65042380/how-to-add-webkit-scrollbar-pseudo-element-in-chakra-ui-element-react](https://stackoverflow.com/questions/65042380/how-to-add-webkit-scrollbar-pseudo-element-in-chakra-ui-element-react)
================================================================================
---
title: React Function Component Definition Rule
description: How to enforce consistent function type for function components.
date: 2021-10-17T00:00:00.000Z
tags: ['react', 'eslint']
id: react-function-component-definition-rule
---
# React Function Component Definition Rule
Example to force using arrow function when defining named components
```js
// .eslintrc.js
{
...,
"rules": {
...,
"react/function-component-definition": [2, {
"namedComponents": ["arrow-function"],
}]
}
}
```
References: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/function-component-definition.md
================================================================================
================================================================================
PORTFOLIO (10)
================================================================================
---
title: Relinvest
subtitle: Reliance Capital Management's Investment Community App
date: 2025-04-01T00:00:00.000Z
stacks: ['Expo', 'React Native', 'NativeWind', 'Gluestack UI', 'SWR', 'Zustand', 'Zod', 'Firebase', 'TypeScript']
id: relinvest-mobile
---
# Relinvest
## Overview
Relinvest is Reliance Capital Management's investment community and simulation platform. Built from scratch with Expo and modern React Native patterns, it features investment simulations, community content, and market data. Shipped to production and available on the Play Store.
## Role & Context
As Tech Lead and Product Engineer, I was responsible for the full engineering lifecycle—from architecture decisions through production deployment. Reliance Capital Management needed a mobile-first investment community platform, and I built both the mobile app and shaped the product direction alongside stakeholders.
## Key Features
### Investment Community
Community content sections (ReliFriend, ReliPrioritas) powered by web views with Remote Config for dynamic URLs.
### Investment Simulation
Goal-based and yield-based investment calculators with pie chart visualizations powered by react-native-gifted-charts. Includes risk profile surveys.
### Market Data
Market data integration with efficient caching and revalidation using SWR and Axios.
### Authentication & Security
JWT-based auth with secure storage via react-native-mmkv.
### Push Notifications
Expo Notifications integration for timely market alerts and community updates.
## Build Notes
### API Response Validation
Every API response is validated through Zod schemas before reaching the UI. Since the backend was being built in parallel, this caught contract mismatches early in development rather than silently breaking in production.
### Web Views for Evolving Content
The community sections (ReliFriend, ReliPrioritas) are WebView screens with URLs configured through Firebase Remote Config. This lets URLs and feature visibility be updated without a full app release.
### Multi-Environment CI/CD
EAS Build with staging, beta, and production channels. Each environment has its own Firebase config and bundle identifier, keeping test data isolated from production.
================================================================================
---
title: ReliID
subtitle: Reliance Capital Management's Financing Platform Super App
date: 2024-08-01T00:00:00.000Z
stacks: ['Expo', 'React Native', 'Tamagui', 'SWR', 'Zustand', 'Zod', 'Firebase', 'TypeScript', 'Vite', 'Ant Design']
id: reliid-mobile-v2
---
# ReliID
## Overview
ReliID is Reliance Capital Management's financing super app. A full-stack product with a mobile app (Expo/Tamagui) and web dashboards (React/Vite/Ant Design), featuring loan applications, payment processing, and user management. Led engineering through the full product lifecycle from prototype to production.
## Role & Context
As Tech Lead and Product Engineer, I built both the mobile experience and internal web dashboards from the ground up. Reliance Capital Management needed a comprehensive financing platform handling the entire loan lifecycle-from application to disbursement-and I led engineering from prototype through production.
## Key Features
### Loan Application Flow
Complete loan application process with document upload (expo-document-picker, expo-camera), ID verification, and real-time status tracking.
### Payment Processing
Integrated payment workflows with PDF statement generation (react-native-pdf, react-native-blob-util) and transaction history.
### User Management
Authentication, profile management, and local authentication (expo-local-authentication) for secure access.
### Internal Dashboards
Web-based admin panels using React, Vite, and Ant Design for managing applications, reviewing submissions, and monitoring platform health.
### Push Notifications
Expo Notifications for application status updates and payment reminders.
## Build Notes
### Dual-Platform Architecture
The mobile app and web dashboards share the same API contracts but are independent codebases - the mobile app uses Expo/Tamagui while the dashboards use React/Vite/Ant Design. Zod schemas enforce API response validation on both sides, keeping contracts consistent without sharing code.
### Document Handling on Mobile
Loan applications require significant document interaction - uploading files (expo-document-picker, expo-camera), viewing PDFs (react-native-pdf), and verifying identity (expo-local-authentication). Each of these native integrations needed careful handling across Android versions and device capabilities.
### Multi-Environment CI/CD
EAS Build with staging, beta, and production channels. Each environment has its own Firebase config and bundle identifier, ensuring test data never leaks into production and OTA updates can be targeted per channel.
================================================================================
---
title: SIApps
subtitle: Inclusive Community Platform App backed by GP Ansor
date: 2024-06-01T00:00:00.000Z
stacks: ['Expo', 'React Native', 'Tamagui', 'SWR', 'Zustand', 'Zod', 'Firebase', 'TypeScript', 'Vite', 'Ant Design']
id: siapps-mobile
---
# SIApps
## Overview
SIApps is an inclusive community platform backed by GP Ansor, serving 50k+ monthly active users across mobile and web. Features include event management, discussion forums, member directories, academy programs, and financial aid modules. Led engineering from architecture to production.
## Role & Context
As Tech Lead and Product Engineer, I built the mobile app and internal web dashboards from scratch. GP Ansor needed a unified platform for their community programs-managing events, membership, education, and financial services-and I delivered a solution that now serves 50k+ monthly active users.
## Key Features
### Home
Dynamic menu with configurable ordering (controlled via Firebase Remote Config) for economy, academy, consultation, vocation, event, and worship sections.
### Community
YouTube video and image gallery with optional external platform integration, all configurable via Remote Config.
### Academy & Ansor Programs
Program listing, application flows, history tracking, and member card management with photo updates and printable cards.
### Economy & Business
Business registration and related master data management for community enterprises.
### Legal & Financial Aid
Dedicated modules for legal aid, tax aid, financial aid, and consultation with submission flows.
### Force Update
App version enforcement via Remote Config `min_version`, ensuring users stay on compatible versions.
### Help Center
WebView to static support pages and WhatsApp integration with predefined messages (configurable via Remote Config).
## Build Notes
### Remote Config as a Feature Toggle
The home menu, community section, and help center all use Firebase Remote Config to control content URLs and visibility. This means the product team can reorder home menu items, toggle community features on and off, or update WhatsApp predefined messages without an app release. This was essential for a community app where program availability changes frequently.
### API Response Validation
Every API response is validated through Zod schemas organized per service module, with centralized enforcement in the API client. When the backend changes a field or adds a nullable property, the app throws a clear error in development rather than silently breaking in production. This pattern was consistent across both the SIApps and Relinvest codebases.
### Multi-Environment CI/CD
EAS Build with staging, beta, and production channels. Force update is handled via a `min_version` Remote Config key - when users are on an outdated version, they're prompted to update rather than encountering runtime errors from API contract changes.
### Scale
50k+ monthly active users across mobile and web platforms.
================================================================================
---
title: Life Insurance Dashboard
subtitle: AJRI
date: 2024-03-20T00:00:00.000Z
stacks: ['React', 'TypeScript', 'Vite', 'Ant Design']
id: insurance-dashboard
---
# Life Insurance Dashboard
## Overview
Internal dashboard for life insurance policy management and claims processing. Built for AJRI (Asuransi Jiwa Reliance Indonesia) to manage the full insurance lifecycle - from policy creation through claims.
## Role & Context
As Tech Lead and sole frontend engineer, I was responsible for the full frontend development - from architecture decisions through to production deployment. The project required extensive data visualization, complex form workflows, and tight integration with backend services.
## Key Features
### Policy Management
Comprehensive policy tracking with complex data tables and filtering. View, search, and manage insurance policies across their lifecycle.
### Claims Processing
Structured claims workflows with document verification, approval routing, and status tracking.
### Underwriting Workflows
Step-by-step underwriting forms with validation, document upload, and decision routing.
### Agent Performance Metrics
Data visualization dashboards tracking agent productivity, sales metrics, and policy conversions.
## Build Notes
### Data-Heavy UI Patterns
Insurance dashboards involve dense data tables, multi-step form workflows, and approval routing - all areas where Ant Design's enterprise components (tables, forms, steps) provide solid defaults. The challenge was customizing these components for insurance-specific workflows while keeping the UX approachable for non-technical staff.
### API Integration
Centralized ky instance with auth interceptors and error handling. Each domain module (policies, claims, underwriting) has its own service layer, keeping the code organized without over-abstracting.
### Development Workflow
Biome for linting/formatting and Vitest for unit tests were set up from the start. Commitlint enforces conventional commits across the team, keeping the git history clean for collaborative development.
================================================================================
---
title: Loan Origination and Management System
subtitle: Reliance Finance internal dashboard and customer platform for lending services.
date: 2023-05-01T00:00:00.000Z
stacks: ['React', 'TypeScript', 'Vite', 'Ant Design', 'SWR']
id: loms-dashboard
---
# Loan Origination and Management System
## Overview
A dual-purpose platform for Reliance Finance: an internal dashboard for loan officers and a customer-facing portal for loan applicants. Covers the entire lending lifecycle - from application intake through approval, disbursement, and repayment tracking.
## Role & Context
As founding engineer and Tech Lead, I built the frontend from scratch - handling everything from requirements gathering to production deployment. I also managed a small team of engineers and coordinated directly with stakeholders.
## Key Features
### Application Intake
Multi-step loan application forms with document upload, validation, and real-time status tracking.
### Approval Workflows
Configurable approval routing with role-based access controls and status tracking.
### Credit Scoring Integration
Real-time integration with backend credit scoring services for risk assessment.
### Data Tables & Reporting
Complex data tables with filtering, sorting, and export for loan portfolio management.
### Customer Portal
External-facing interface for applicants to check loan status, upload documents, and view repayment schedules.
## Build Notes
### Data Fetching Strategy
SWR handles all API data fetching with caching and revalidation. This was particularly useful for lending workflows where loan status changes frequently - officers need to see up-to-date information without manually refreshing the page, but also shouldn't hammer the backend with unnecessary requests.
### Dual Audience
The platform serves two distinct audiences: internal loan officers who need complex workflows, data tables, and approval tools, and external applicants who need a simple, guided experience. Each audience gets its own interface layer while sharing the same API contracts and validation logic.
### Monorepo Considerations
With only two frontend targets (dashboard and customer portal) sharing similar patterns, a full monorepo setup wasn't justified. Instead, shared component patterns are documented and replicated across both codebases.
================================================================================
---
title: Mortgage Application and Calculator
subtitle: Pinhome
date: 2023-03-01T00:00:00.000Z
stacks: ['Next.js', 'TypeScript', 'Micro Frontend']
id: pinhome-mortgage
---
# Mortgage Application and Calculator
## Overview
Mortgage and takeover simulator for property owners and buyers. Part of Pinhome's property ecosystem, helping users calculate loan repayments, compare mortgage products, and start applications.
## Role & Context
As a Software Engineer at Pinhome, I worked on the mortgage simulator and application flow - a micro-frontend integrated into Pinhome's broader property platform. The focus was on building a fast, SEO-friendly mortgage calculator that feeds into the broader application pipeline.
## Key Features
### Mortgage Calculator
Interactive loan simulation with adjustable parameters - loan amount, tenure, interest rate, and down payment.
### Mortgage Takeover Simulator
Compare and simulate mortgage takeover scenarios from one bank to another.
### Application Flow
Smooth handoff from simulation results into the mortgage application process.
## Build Notes
### Micro-Frontend Architecture
The mortgage simulator and application flow live as a micro-frontend within Pinhome's broader property platform. This means the mortgage module has its own build and deployment cycle, independent of the parent app shell. Next.js server-side rendering ensures the mortgage pages are SEO-friendly - critical for a product where users often discover KPR calculators through search.
### SEO-Driven Product Decision
Mortgage calculators are high-intent search terms. Building this as a server-rendered micro-frontend rather than a pure client-side app was a deliberate product decision to ensure discoverability through organic search.
================================================================================
---
title: [Undisclosed Bank] Corporate Portal
subtitle: [Undisclosed Contractor] - [Undisclosed Bank]
date: 2022-05-24T00:00:00.000Z
stacks: ['React', 'Chakra-UI']
id: 2022-01-undisclosed-bank-corporate-portal
---
# [Undisclosed Bank] Corporate Portal
## Overview
A corporate banking portal for corporate clients of a major Indonesian bank. Provides corporate account management, cash flow monitoring, and approval workflows for business banking operations.
## Role & Context
As a sub-contracted frontend engineer, I led the web frontend development and provided consultancy for the overall frontend architecture. The project required a polished, enterprise-grade experience with complex data flows and strict security requirements.
## Key Features
### Corporate Dashboard
Configurable widget dashboard displaying deposit totals, loan summaries, cash flow trends, and account activity from a corporate client's perspective.
### Approval Workflows
Maker/approver role-based approval system for corporate transaction workflows.
### User Management
Corporate client user administration with role-based permissions.
### Secure Authentication
RSA public key encryption for login credentials, CAPTCHA integration, and session management.
## Build Notes
### Security Requirements
Banking projects come with strict security requirements. Login credentials are encrypted client-side using RSA public key encryption before transmission — the server never sees raw passwords. CAPTCHA adds bot protection, and sessions are managed carefully to prevent hijacking.
### Data-Heavy Dashboard
The dashboard displays deposit totals, loan summaries, cash flow trends, and account activity through configurable widgets. Chart.js renders the data visualizations, and react-intl handles bilingual support (English and Indonesian) since the portal serves both internal and corporate client users.
### Widget-Based Layout
The home page uses a configurable widget system where different user roles see different sets of widgets. This means the same foundation serves multiple use cases without building separate applications.
================================================================================
---
title: Pinhome Mobile App
subtitle: Property Search and Transaction App
date: 2022-01-01T00:00:00.000Z
stacks: ['Flutter']
id: pinhome-mobile-app
---
# Pinhome Mobile App
## Overview
Pinhome is a property search and transaction app with 500K+ downloads on the Play Store. It helps users discover properties, simulate mortgage payments, and apply for home loans - all within a single mobile experience.
## Role & Context
As a Software Engineer on the Mortgage Tribe, I contributed to the Flutter mobile app, building features for loan simulations, mortgage applications, and property discovery. I also worked on the internal SDK used across multiple features in the app.
## Key Features
### Mortgage Simulator
Loan calculation and mortgage simulation tools for property buyers.
### Property Discovery
Search and browse properties with filtering and detail views.
### Mortgage Applications
End-to-end mortgage application flow from submission to tracking.
### Internal SDK
Contributed to the shared SDK regulating common patterns across multiple feature teams.
## Build Notes
### Team Structure
Worked within the Mortgage Tribe at Pinhome alongside other feature teams. The shared internal SDK regulated common patterns - navigation, theming, and data fetching conventions - so that features built by different teams remained consistent across the app.
### Mobile-First Mortgage Experience
Mortgage workflows are inherently multi-step with document submissions, calculations, and status tracking. The mobile experience needed to guide users through each step clearly while handling the complexity of different loan products and bank partnerships.
================================================================================
---
title: JPCC.org Video Event Platform
subtitle: Skybridge - Studio Alva
date: 2021-09-15T00:00:00.000Z
stacks: ['Next.js', 'Chakra-UI']
id: jpcc-video-platform
---
# JPCC.org Video Event Platform
## Overview
A video event platform for JPCC.org, enabling video streaming and on-demand content consumption. Built for one of Indonesia's largest church communities to deliver sermons, events, and conferences to their congregation online.
## Role & Context
As a sub-contracted frontend engineer through Studio Alva, I led the web frontend development - building the video player experience, live chat, and user engagement features. Worked closely with the design team to deliver a polished streaming experience.
## Key Features
### Video Streaming
Video playback powered by video.js and Bitmovin Player, with quality selection and YouTube integration.
### Live Chat
Real-time chat during events using Pusher for WebSocket communication, with emoji reactions and interactive engagement.
### Event Pages
Server-rendered event listing and detail pages for all services and events.
### Social Sharing
Share buttons via react-share for spreading event pages.
## Build Notes
### Dual Video Player Setup
Two video player integrations were needed: video.js for standard YouTube content with quality selection, and Bitmovin Player for premium streaming features. This gave flexibility for different content types - sermon recordings handled well by video.js, while special events needed Bitmovin's more capable player.
### Server-Rendered Content
All content pages use server-side rendering via `getServerSideProps`. This ensures event pages always show up-to-date information (schedule changes, new events) without needing to rebuild or redeploy. Video and chat components hydrate client-side for interactive features that don't need SEO.
### Real-Time Chat
Live chat during events uses Pusher for WebSocket communication. The chat component supports emoji reactions and integrates with the video stream timing.
================================================================================
---
title: JSM Warehouse System
subtitle: CV. Jayasakti Mandiri
date: 2021-07-04T00:00:00.000Z
stacks: ['React', 'Chakra-UI', 'Ionic', 'Capacitor', 'Express', 'PostgreSQL']
id: jsm-warehouse-system
---
# JSM Warehouse System
## Overview
A warehouse management system for CV. Jayasakti Mandiri (JSM), a company that sells agricultural tools and provides related services. The system modernizes their warehousing flow from manual logging into a centralized platform with web and mobile apps.
## Role & Context
As contractor and Project Lead, I handled the full product - from UI design through to web frontend, mobile app, and backend development. I led a team of 3 engineers including myself, coordinating requirements gathering with stakeholders and managing delivery timelines.
## Key Features
### Inventory Management
Product tracking with stock levels, categories, and search across the warehouse catalog.
### Order Processing
End-to-end order handling from intake through fulfillment and delivery tracking.
### Mobile App
Cross-platform mobile app for warehouse staff to manage inventory on the floor using barcode scanning and camera capture.
### Reporting
Sales and inventory reports with filtering and export capabilities.
## Build Notes
### Full-Stack Ownership
As the project lead, I owned the entire product end-to-end - UI design in Figma, web dashboard with React/Chakra UI, mobile app with Ionic/Capacitor, and the Express/PostgreSQL backend. This removed cross-team coordination overhead and let us iterate quickly based on direct stakeholder feedback from JSM's team.
### Mobile for the Warehouse Floor
The mobile app needed to work on devices used by warehouse staff - barcode scanning for inventory lookup, camera for document capture, and simple data entry forms that work with gloves. Capacitor provided the bridge to these native device capabilities without maintaining separate iOS and Android codebases.
================================================================================
================================================================================
PROJECTS (20)
================================================================================
---
title: skills
description: A collection of agent skills for AI coding agents.
date: 2026-06-17T00:00:00.000Z
stacks: []
id: skills
---
# skills
## Overview
A collection of agent skills for AI coding agents, distributed via the `skills` CLI ecosystem. Skills are reusable instruction packages that teach agents how to handle specific types of work - with phases, guardrails, and success criteria. The collection currently includes an architecture decision framework, with more domains planned.
## Why I Built This
AI agents are only as good as the methodology they follow. I wanted a way to package engineering discipline patterns - like structured decision-making - into sharable, installable instructions that any agent can load on demand. The skills CLI makes distribution trivial: one command and the skill is available for any session.
## Key Features
#### Domain-Specific Methodology
Each skill encodes a complete workflow for a specific task type - with phases, guardrails, and success criteria - so agents follow a repeatable process rather than improvising.
## Build Notes
#### General Agent-Skills Format
The project follows the general agent-skills specification - markdown content with YAML frontmatter, registered via a plugin manifest.
#### Reference Document Pattern
Heavy reference data (scoring profiles, templates, lookup tables) lives in companion files that agents fetch on demand, keeping the main skill focused on the workflow.
================================================================================
---
title: maestria
description: Portable AI engineering praxis, encoded as plugins.
date: 2026-06-12T00:00:00.000Z
stacks: ['typescript', 'astro', 'starlight', 'pnpm', 'vitest']
id: maestria
---
# maestria
## Overview
Maestria is an open-source monorepo that packages AI engineering methodology - design patterns, agent prompts, workflow rules - as reusable, installable plugins for AI coding agents. The first published package, `@maestria/opencode`, transforms OpenCode into a disciplined AI engineering workstation with structured handoffs, maker/checker splits, and pipeline-based orchestration.
The core insight: _Agent = Model + Harness._ The model provides capability; the harness provides reliability. Most agent failures are harness failures, not model failures.
## Why I Built This
After months of daily AI-assisted engineering, I noticed a pattern: the same reliability issues kept repeating. Agents would assume instead of verifying, skip documentation, or validate their own work without a second pair of eyes. These weren't model capability problems - they were process problems.
I extracted the discipline patterns that worked, formalized them into reusable agent prompts, and packaged them as a plugin. The result is a behavior layer that makes AI coding agents more reliable by default.
## Key Features
#### Eight Specialized Subagents
A manager/dispatcher (`@orchestrator`) delegates to seven specialists - `@adventurer` for recon, `@architect` for design, `@planner` for planning, `@builder` for implementation, `@reviewer` for QA, `@diagnose` for debugging, and `@writer` for documentation. Each has a focused toolset and clear ownership boundaries.
#### Pipeline Architecture
Structured handoffs chain agents together: Recon → Design → Implement → Validate. Each handoff includes six fields - Goal, Context, Requirements, Known Problems, Success Criteria, Next Step - preventing dropped context between agents.
#### Maker/Checker Split
The agent that produces work cannot validate it. The reviewer operates with `edit: deny` enforced at the permission level, ensuring a genuine second opinion on every change.
#### Self-Wiring Installation
A single line in `opencode.jsonc` - `"plugin": ["@maestria/opencode@latest"]` - auto-installs the entire system from npm. No manual configuration, no file copying.
## Build Notes
#### Pure Plugin Architecture
The plugin uses exactly two hooks - `config` (register agents + inject rules) and `session.compacting` (preserve session state). No postinstall scripts, no file copying, zero side effects. Agents are served as markdown files from the npm package directly, keeping them human-readable without a TypeScript factory layer.
#### ADR-Driven Development
Every significant design decision is documented as an Architecture Decision Record, from global rules scope to tool permission design. Seven ADRs trace the reasoning behind the architecture, making the project's evolution transparent.
================================================================================
---
title: CarTrack
description: Local-first vehicle consumption tracker for EV and gasoline cars.
date: 2026-05-28T00:00:00.000Z
stacks: ['react', 'tanstack-router', 'tanstack-query', 'zustand', 'dexie', 'tailwindcss', 'vite', 'typescript']
id: cartrack
---
# CarTrack
## Overview
CarTrack is a local-first web application for tracking vehicle consumption-both electric vehicles (EV battery) and gasoline cars. It logs odometer readings, battery/fuel levels, and charging/refueling events, then calculates consumption rates and predicts range and days until empty.
All data lives in the browser's IndexedDB. No accounts, no backend, no tracking.
## Why I Built This
I wanted a simple, private way to track my car's consumption over time without relying on cloud services or car manufacturer apps. Existing solutions don't support both EV and gasoline vehicles in one place. Building my own gave me full control over the data and the experience.
## Key Features
### Dual Vehicle Support
Track both electric vehicles and gasoline cars in one app. Each vehicle type has tailored consumption metrics-km per % for EVs, km per liter for gasoline. PHEV (plug-in hybrid) is also supported.
### Consumption Calculations
Rolling-window consumption rate with median + outlier detection for accurate, stable readings over time. Later evolved to charge-cycle-based computation-calculating consumption per full charge cycle instead of per reading pair, using median for noise filtering.
### Predictions
Estimated range and days until empty, using a hybrid approach: user-configured daily distance combined with auto-calculated averages from reading history.
### Fuel Event Logging
Log charging sessions or refueling events with cost and amount tracking for cost-per-km trends. Auto-detects event type from vehicle type (EV → charge, Gasoline → refill).
### Charts
Visualize consumption over time, cost per km trends, and charge cost trends with interactive charts.
### Data Export & Import
Full JSON backup or per-vehicle CSV export. Import is idempotent with hash deduplication-no duplicate entries.
## Build Notes
### Local-First with Dexie.js
All data is stored in IndexedDB via Dexie.js. The schema is simple-vehicles, readings, and fuel events-with compound indexes for efficient queries. No server means the app works fully offline and loads instantly. One key learning: use explicit string UUIDs (`'id'`) for Dexie primary keys, not auto-increment (`'++id'`), to avoid conflicts during import.
### Getting Better Over Time
Consumption accuracy improved significantly-early versions averaged readings naively, now it understands full charge cycles and filters out noise. The app grew from a simple tracker to a proper PWA you can install and use offline, across multiple platforms.
================================================================================
---
title: PahamiDulu
description: Bilingual health awareness app for common conditions and special needs.
date: 2026-04-13T00:00:00.000Z
stacks: ['TanStack Start', 'TanStack Router', 'Tailwind CSS v4', 'shadcn/ui', 'TypeScript', 'Zustand', 'Vite']
id: pahamidulu
---
# PahamiDulu
## Overview
PahamiDulu is a bilingual (Bahasa Indonesia + English) health awareness web application designed to educate the public about common health conditions and special needs. The app helps reduce stigma, prevent mishandling, and guide people to professional help.
The name "PahamiDulu" means "Understand First" in Indonesian - reflecting the core mission: before judging or acting, understand.
## Why I Built This
Health misinformation and stigma are prevalent in Indonesia. Many people don't know how to respond appropriately to conditions like autism, ADHD, hypertension, or diabetes. PahamiDulu bridges this gap by providing clear, actionable guidance in a language people understand.
## Key Features
### Bilingual Content
All content is available in both Bahasa Indonesia and English, with a language toggle that persists across sessions.
### Health Topics Covered
**Common Conditions:** Hypertension, cholesterol, gout, obesity, diabetes, anxiety
**Special Needs & Syndromes:** Autism, ADHD, Tourette syndrome, Down syndrome
### Structured Guidance
Each topic includes symptoms, causes, what helps, what to avoid, how to respond in various scenarios, red flags requiring professional help, and scientific citations.
### Provider Directory
A directory of telehealth partners and clinics to help users find professional help.
## Approach
### Content-First Architecture
All health content lives in data files, and routes render from them. This separation of content from presentation makes it easy to add new topics without touching application code, and enables non-technical contributors to update content.
### Bilingual Strategy
Content uses a simple `{ id: string; en: string }` structure rather than a full i18n library - appropriate for content that doesn't change frequently. Language preference persists in localStorage via Zustand.
================================================================================
---
title: Tools
description: Collection of browser-based utilities.
date: 2026-03-31T00:00:00.000Z
stacks: ['vite', 'react', 'typescript', 'tailwindcss', 'tanstack-router', 'pnpm', 'turbo']
id: tools
---
# Tools
## Overview
tools.sznm.dev is a unified web application that consolidates multiple browser-based utilities into a single, fast, privacy-focused platform. Instead of maintaining half a dozen separate micro-apps, everything lives in one codebase with shared UI primitives and a consistent user experience.
The app includes six tools:
- **WA Link Helper** - Generate WhatsApp links with pre-filled messages and international phone number support
- **Zippy Image** - Compress images client-side with no server upload
- **UA Check** - Inspect browser and device user agent information
- **QR Code Generator** - Create QR codes for URLs and vCard contacts
- **JS Perf Comparator** - Benchmark JavaScript snippets in a sandboxed QuickJS runtime
- **Add to Calendar** - Build Google Calendar event links with timezone handling
## Why I Built This
I had several standalone utility apps (WA Link Helper, QRcodeGen, Add to Calendar Generator, etc.) that were each their own Next.js projects. Maintaining them separately meant duplicated dependencies, inconsistent UI, and fragmented deployment pipelines. When I wanted to add new tools, the overhead of spinning up another repo was discouraging.
Consolidating everything into a single Vite + React app with a modular package architecture solved this. Each tool's core logic lives in its own workspace package, while the web UI shares a single design system. New tools can be added by creating a new package and a route file - no new repo, no new deployment pipeline.
## Key Features
### Unified Tool Directory
A clean web aoo with quick access to all six tools. No navigation complexity - find what you need and start using it immediately.
### Privacy-First Architecture
All processing happens in the browser. Image compression, QR code generation, JavaScript benchmarking, and calendar link building - none of your data leaves your device. No analytics, no tracking, no server-side processing.
### Modular Package Design
Each tool's business logic is extracted into a separate workspace package (`@toolbox/calendar-core`, `@toolbox/qrcode-core`, etc.). This keeps the web app thin and makes the core logic testable and reusable.
### Sandboxed JavaScript Benchmarking
The JS Perf Comparator runs code inside QuickJS via Web Workers, providing isolated execution for fair comparisons. No `eval()`, no global scope pollution - just controlled sandboxed runs with statistical aggregation.
### PWA Support
Installable as a Progressive Web App for offline access to tools that don't require network connectivity.
## Build Notes
### Monorepo with pnpm Workspaces
Built as a pnpm workspace monorepo with Turborepo for task orchestration. The `apps/toolbox-web` directory contains the Vite + React frontend, while `packages/*` houses each tool's core logic and shared configuration.
### Vite+ Toolchain
Uses Vite+ (a unified toolchain wrapping Vite, Vitest, Rolldown, and Oxlint) for development, building, and testing. This replaces separate tool installations with a single `vp` CLI.
### Design System on React Aria Components
UI primitives are built on `react-aria-components` with Tailwind CSS 4 for styling. This provides accessible, keyboard-navigable components out of the box without heavy component library dependencies.
### File-Based Routing
TanStack Router's file-based routing organizes tools under the `/_tools` layout route. Adding a new tool is as simple as creating a new route file - the router handles code splitting automatically.
### Current Status
Actively maintained. New tools are added as needs arise, and existing ones are refined based on usage. The consolidated architecture makes iteration significantly faster than the previous micro-app approach.
================================================================================
---
title: SavorSanctum
description: Curated directory for culinary recommendations.
date: 2025-12-07T00:00:00.000Z
stacks: ['tanstack-start', 'tanstack-router', 'tailwindcss', 'notion']
id: savorsanctum
---
# SavorSanctum
## Overview
SavorSanctum is a curated directory for culinary recommendations-restaurants, cafes, specialty food products, and gift ideas. It serves as a centralized place for discovering and sharing food-related recommendations.
## Why I Built This
Rather than scattered notes across different apps, a dedicated directory for food recommendations made more sense. It allows for organized discovery and easy sharing of culinary finds.
## Key Features
### Curated Directory
Browse and search culinary recommendations organized by category.
### Search & Filter
Find places by cuisine type, location, or occasion using Fuse.js for fuzzy search.
### Data from Notion
Content managed in Notion and fetched via the Notion API, making updates simple.
### Responsive Design
Built with modern tools for a fast, mobile-friendly experience.
## Build Notes
### Content as Configuration
All restaurant and food data lives in a Notion database, fetched at build time via the Notion API. This means content updates - adding a new restaurant, updating hours, changing a recommendation - happen in Notion and propagate on the next build, without touching any code.
### Fuzzy Search
Client-side search powered by Fuse.js lets users find places by partial name matches or cuisine type. Since the dataset is small enough to load entirely client-side, there's no need for a search backend.
================================================================================
---
title: Konsu
description: Local-first household consumables tracker with smart predictions.
date: 2025-09-09T00:00:00.000Z
stacks: ['react', 'tanstack-router', 'tinybase', 'zustand', 'tailwindcss', 'vite', 'typescript']
id: konsu
---
# Konsu
## Overview
Konsu is a local-first web app for tracking household consumables-the items you regularly need to buy, refill, replace, or recharge. Toothbrush heads, batteries, razor blades, face wash, air filters, and more. It predicts when items need restocking based on usage patterns and provides smart reminders.
No accounts, no cloud sync, no internet dependency. Just your data, in your browser.
## Why I Built This
This started from a real problem: I had scattered text notes across different apps about when I last bought batteries, replaced my toothbrush head, or restocked razor blades. Every time something ran out, I'd realize I'd forgotten to track it. A dedicated tracker with automatic predictions seemed obvious-but every existing app either required cloud accounts or was overcomplicated for something so simple.
The core insight: the more you use Konsu, the smarter it gets. Every restock event teaches the prediction engine your actual usage patterns, turning manual tracking into automatic foresight.
## Key Features
### Component-Level Tracking
Items can have sub-components (e.g., brush heads for an electric toothbrush). Track each component's lifecycle independently with per-component predictions. A toothbrush handle lasts years; its brush heads need replacing every 3 months. Konsu tracks both.
### Semantic Action Verbs
History entries use meaningful verbs-New, Restock, Replace, Charge, Empty, Service, Other-instead of a single "restock" concept. This enables fine-grained lifecycle tracking per item. Replacing razor blades is different from recharging batteries, and the data reflects that.
### Mobile-First UX
Bottom navigation, bottom sheets instead of modals, swipe gestures, and thumb-zone-optimized layouts. Designed for one-handed use on a phone.
### Accessibility-First
WCAG 2.1 AA compliance: 44px minimum touch targets, full keyboard navigation, screen reader support, and `prefers-reduced-motion` respect. This wasn't an afterthought-it was a hard requirement from the start.
### Onboarding
A welcome screen with feature carousel helps new users understand the app without a manual. Items can be seeded from templates to solve the cold-start problem.
### Backup & Restore
Export and import your data. No vendor lock-in.
### PWA
Installable to home screen with full offline support.
## Build Notes
### TinyBase for Local-First Data
TinyBase provides a reactive, relational data store that persists to IndexedDB. Unlike traditional state management, TinyBase handles both the in-memory store and persistence layer, making reactive queries trivial. The schema tracks items, components, history entries, and categories.
================================================================================
---
title: zippy
description: Client-side image compression tool.
date: 2025-05-16T00:00:00.000Z
stacks: ['Vite', 'react', 'tailwindcss', 'shadcn']
id: zippy
---
# zippy
## Overview
zippy is a client-side image compression tool that reduces file sizes directly in the browser. No uploads to external servers-everything happens locally, ensuring privacy and speed.
## Why I Built This
I frequently needed to compress images for web use but didn't want to upload them to third-party services due to privacy concerns. Existing tools either required uploads, had file size limits, or were filled with ads.
A browser-based solution using modern compression libraries seemed like the perfect approach.
## Key Features
### Client-Side Processing
Images are compressed locally in the browser using browser-image-compression - nothing leaves your device. Perfect for sensitive screenshots or proprietary designs.
### Multiple Format Support
Handles JPEG, PNG, and WebP formats with quality adjustment controls.
### Batch Processing
Compress multiple images at once with progress indicators.
### Size Comparison
Visual before/after comparison showing file size reduction percentage.
### Privacy First
No server uploads, no tracking, no ads. Your images stay on your device.
## Build Notes
### Client-Only Architecture
Everything runs in the browser - image compression via browser-image-compression, batch download via JSZip and file-saver. No server, no uploads, no tracking. The tradeoff is that very large batches can strain browser memory, but for typical use (a few images at a time), this works perfectly.
### Current Status
Recently built and actively maintained. A utility I use regularly in my own workflow.
================================================================================
---
title: xtarterize
description: CLI for automated project setup and configuration scaffolding.
date: 2025-04-20T00:00:00.000Z
stacks: ['typescript', 'nodejs', 'pnpm', 'turbo', 'vite', 'vitest', 'biome']
id: xtarterize
---
# xtarterize
## Overview
xtarterize is a CLI tool that detects your JavaScript/TypeScript project stack automatically, then applies curated production-grade configurations for linting, type checking, CI workflows, code generation, editor settings, and more - all without destructively overwriting your existing setup.
Run `npx xtarterize init` and your project gets Biome, TypeScript incremental builds, Renovate, commitlint, VS Code settings, GitHub Actions, and more - tailored to your specific stack.
## Why I Built This
I found myself repeatedly copying configuration files between projects - ESLint configs, Prettier setups, GitHub Actions workflows, TypeScript settings. Each new project required the same boilerplate setup, and keeping all those configs in sync across dozens of repos was unmaintainable.
Rather than maintaining templates that quickly go stale, I wanted a tool that could inspect any existing project and apply the right configurations dynamically. xtarterize solves this by detecting your stack and applying only what you need.
## Key Features
### Stack Detection
Reads `package.json`, lockfiles, and existing config files to build a `ProjectProfile`. Supports React, React Native, Vue, Svelte, Solid, Node.js, Vite, Next.js, Expo, TanStack Start, Tailwind, and more.
### Non-Destructive Application
All configurations are applied using deep merge and AST patching. Existing content is preserved, and every modified file is backed up to `.xtarterize/backups/`.
### Dry-Run First
Always see exactly what will change before applying anything. No surprises.
### Idempotent
Running `xtarterize init` twice changes nothing on the second run. Safe to run in CI or on cron.
### Task Categories
Covers linting (Biome), TypeScript (incremental builds, strict mode), CI/CD (GitHub Actions), dependencies (Renovate), release (commitlint, czg), quality (Knip), codegen (Plop), monorepo (Turborepo), editor settings, and AI agent configs (AGENTS.md).
## Build Notes
### Monorepo Architecture
Built as a pnpm workspace monorepo with three core packages: `@xtarterize/core` for detection and task interface, `@xtarterize/patchers` for config file manipulation, and `@xtarterize/tasks` for all task implementations. The CLI itself lives in `apps/cli` using `citty` and `@clack/prompts`.
### Real Templates from Production
Every configuration template is derived from actual production projects, not theoretical examples. This ensures the configs work in real-world scenarios with real dependency versions.
### Modular Task System
Each task implements a simple interface: `applicable(profile)`, `check(cwd, profile)`, `dryRun(cwd, profile)`, and `apply(cwd, profile)`. Adding new tasks is straightforward - implement the interface and export it.
================================================================================
---
title: Leeboor
description: Indonesian holiday calendar with long weekend detection.
date: 2023-09-19T00:00:00.000Z
stacks: ['astro', 'react', 'tailwindcss']
id: leeboor
---
# Leeboor
## Overview
Leeboor is a calendar application focused on Indonesian national holidays. It helps identify long weekends by showing which holidays fall near Fridays or Mondays, making it easier to plan time off.
## Why I Built This
Planning around Indonesian holidays required manually checking calendars to spot long weekend opportunities. A dedicated tool that highlights these patterns simplifies vacation planning.
## Key Features
### Holiday Calendar
View Indonesian national holidays organized by year.
### Calendar View
Alternative calendar interface for browsing holidays visually.
### Long Weekend Planning
Easily identify holidays that create extended weekends.
## Build Notes
### Astro with React Islands
Astro generates static HTML at build time, with React components hydrating only where interactivity is needed (the calendar). Holiday data is fetched at build time, so the page loads instantly with no client-side data fetching. This approach works well for data that changes at most once a year.
================================================================================
---
title: [Dart] sznm_lints
description: Personal Dart linter configurations.
date: 2022-10-18T00:00:00.000Z
stacks: []
id: dart_sznm_lints
---
# [Dart] sznm_lints
================================================================================
---
title: xtarter
description: Collection of starter templates.
date: 2022-05-19T00:00:00.000Z
stacks: ['nextjs', 'chakra-ui']
id: xtarter
---
# xtarter
================================================================================
---
title: tungpajak
description: Personal tax calculator and simulator.
date: 2022-03-12T00:00:00.000Z
stacks: ['nextjs', 'chakra-ui']
id: tungpajak
---
# tungpajak
================================================================================
---
title: Personal Site
description: My personal page.
date: 2021-10-08T00:00:00.000Z
stacks: ['svelte']
id: personal-site
---
# Personal Site
================================================================================
---
title: wussh
description: Personalized link page generator.
date: 2021-05-07T00:00:00.000Z
stacks: ['nextjs', 'chakra-ui', 'firebase', 'zustand']
id: wussh
---
# wussh
## Overview
wussh is a link page generator that lets users create personalized landing pages for sharing all their important links. Similar to Linktree but with custom branding and full control.
## Why I Built This
I wanted a customizable way to share multiple links without relying on third-party services with their branding restrictions or subscription requirements. Building my own solution gave complete control over design, features, and hosting.
## Key Features
### Link Management
Create and organize links with custom titles and ordering.
### Drag-and-Drop Ordering
Intuitive interface for rearranging links using react-beautiful-dnd.
### Customizable Themes
Personalize the look and feel to match individual branding.
### Authentication
Secure user accounts powered by Firebase Auth.
### Social Links
Dedicated sections for social media profiles.
## Build Notes
### ISR Experimentation
This was one of the projects where I experimented with Next.js ISR (Incremental Static Regeneration) — exploring stale-while-revalidate HTTP caching headers and when to use ISR versus client-side data fetching for user-generated content. The question: should each user's link page be pre-rendered at build time, or fetched on request? For a link page generator where pages change rarely, ISR was a good fit.
### Current Status
Functional and maintained. One of my earlier production applications using Next.js and Firebase.
================================================================================
---
title: spoker
description: Real-time multiplayer scrum poker for remote teams.
date: 2021-02-28T00:00:00.000Z
stacks: ['nextjs', 'react', 'chakra-ui', 'firebase', 'typescript', 'zustand']
id: spoker
---
# spoker
## Overview
spoker is a real-time multiplayer Scrum Poker application built from the ground up using Next.js, TypeScript, and Chakra UI. It enables teams to estimate story points collaboratively with real-time synchronization.
The app had organic adoption in its early days. While usage has slowed since, I continue to maintain and evolve it. It's become a personal playground for trying out new ideas and staying sharp with modern React patterns.
## Why I Built This
I built this to improve upon the scrum poker experience my office EPD (Engineering-Product-Design) team were using during backlog planning. I saw opportunities to enhance the interface design, responsiveness, and overall reliability-so I decided to build a better version tailored to our team's needs.
I prototyped it in a week and iterated on it based on user feedback.
## Key Features
### Real-Time Collaborative Rooms
Multiple users join a room and see updates instantly using Firebase Realtime Database for synchronization.
### Anonymous Voting
Votes remain hidden until all participants vote or the moderator chooses to reveal, preventing anchoring bias.
### Role-Based Access
Support for different permission levels including room owners, participants, and observers.
### Task Management
Queue-based workflow for organizing and tracking estimation tasks.
## Build Notes
### Real-Time Synchronization
Firebase Realtime Database syncs votes and room state across all connected clients instantly. No custom server needed - Firebase handles the real-time infrastructure.
### Architecture for Future Features
The codebase is structured to allow adding future features (analytics, integrations, paid tiers) without major rewrites. The current tech stack is Next.js and Chakra UI v3, kept up to date as a personal playground for modern React patterns.
### Shipped in a Week
The initial prototype was built in one week and shared publicly. Usage has since slowed, and it now serves as a personal playground for trying out new ideas and keeping up with modern React patterns.
================================================================================
---
title: Base
description: Personal knowledge base.
date: 2021-01-21T00:00:00.000Z
stacks: ['nextjs']
id: base
---
# Base
================================================================================
---
title: Greetings as a Service (GaaS)
description: Generate personalized greeting messages.
date: 2021-01-01T00:00:00.000Z
stacks: ['nextjs', 'chakra-ui']
id: greet-gaas
---
# Greetings as a Service (GaaS)
================================================================================
---
title: muvees
description: Movie database for browsing and discovering films.
date: 2020-12-29T00:00:00.000Z
stacks: ['nextjs', 'chakra-ui', 'swr']
id: muvees
---
# muvees
## Overview
muvees is a movie database application built for browsing films, viewing details, and discovering new movies to watch. It integrates with external movie APIs to provide comprehensive film information.
## Why I Built This
I wanted to experiment with API integration and data fetching patterns in Next.js. This project served as a practical way to learn SWR for data fetching and explore building a polished UI around external data sources.
## Key Features
### Movie Discovery
Browse and search for movies with detailed information including synopsis, cast, and ratings.
### Category Browsing
Explore movies organized by different categories and sections.
### Responsive UI
Built with Chakra UI for a consistent, accessible interface across devices.
## Build Notes
### ISR Experimentation
This project was a testing ground for Next.js ISR (Incremental Static Regeneration). The question: when should you pre-render pages at build time versus fetching data client-side? For movie data that doesn't change frequently, ISR with `stale-while-revalidate` caching headers gave the best balance — instant page loads from cache, with background updates when data changes.
### Evolution
A long-running project that has evolved alongside my learning. The recent upgrade to Chakra UI v3 brought significant styling and theming improvements over v2.
================================================================================
---
title: Public APIs
description: Searchable directory of 1000+ free public APIs.
date: 2020-12-25T00:00:00.000Z
stacks: ['Next.js', 'tailwindcss', 'radix-ui', 'typescript', 'swr']
id: pub-apis
---
# Public APIs
## Overview
Public APIs is a searchable directory that helps developers discover free APIs for their projects. Instead of browsing through GitHub repositories or scattered lists, users can filter by category, authentication requirements, and HTTPS support.
The project gained significant traction on Product Hunt, receiving badges for Top Post of the Day and Golden Kitty nomination.
## Why I Built This
I kept finding interesting APIs in GitHub's public-apis repository but struggled to discover them efficiently. The markdown list became unwieldy, and I wanted:
- **Category filtering**: Quickly find all "Music" or "Finance" APIs
- **Auth visibility**: See at a glance which APIs require keys
- **HTTPS indicator**: Security-conscious selection
- **Fast search**: Instant results without page reloads
This was also an experiment in building a lightweight, content-focused app with Next.js static generation.
## Key Features
### Search & Filter
Real-time search across API names, descriptions, and categories. Filter by:
- Category (40+ categories from Animals to Weather)
- Authentication type (None, API Key, OAuth, etc.)
- HTTPS support
- CORS availability
### Responsive Grid Layout
APIs displayed in a clean card grid that adapts from single column (mobile) to 3 columns (desktop). Each card shows essential info at a glance.
### Dark Mode Support
Full dark mode with Tailwind's dark variant. The UI automatically respects system preferences.
### PWA Ready
Configured as a Progressive Web App with offline support, installable on mobile devices.
### External Links
Direct links to API documentation and source repositories, making it easy to explore further.
## Build Notes
### Static Generation Strategy
The app fetches API data at build time using `getStaticProps`, creating a static site that loads instantly. Data updates trigger a new build via webhook - no database, no server runtime, just static files served from CDN.
### Tech Evolution
Originally built with Chakra UI, the app was migrated to Tailwind CSS + Radix UI primitives. The motivation was bundle size reduction and more flexibility in styling - Chakra's component abstractions weren't needed for what's essentially a filterable list with search.
### Data Source
Powered by the [public-apis](https://github.com/davemachado/public-api) project by Dave Machado. The site provides a searchable interface to this open dataset.
### Recognition
- [Product Hunt: #1 Product of the Day](https://www.producthunt.com/posts/public-apis-3)
- [Product Hunt: Golden Kitty Award Nominee](https://www.producthunt.com/golden-kitty-awards/2021/developer-tools)
- Featured in various developer newsletters
### Current Status
Stable and in maintenance mode. The data updates automatically from the upstream source. Occasionally refreshed with UI improvements and dependency updates.
================================================================================