I've been building Bi Kaizen (a minimalist habit tracker) as a side project over the past few months.
The Stack
- React 19 + TypeScript + Vite
- AWS Amplify Gen 2 (AppSync/GraphQL, DynamoDB, Lambda, Cognito)
- TanStack React Query 5 for caching and optimistic updates
- Tailwind CSS
- dnd-kit for drag-and-drop habit reordering
- i18next for EN / UK / RU support
Here's what I ran into and what actually worked.
1. Amplify Gen 2 has a silent pagination bug you need to fix manually
The code-first schema is genuinely better than YAML/JSON β you get typed GraphQL operations for free and it feels like real engineering. But there's a nasty default behavior that bit me hard.
The problem: Amplify's owner-based auth (allow.owner()) applies authorization after the DynamoDB query, not during it. That means when a user calls listHabits, AppSync executes a full table scan - iterating through every record from every user - hits DynamoDB's pagination limit (e.g. 1MB of data), and then filters down to just the records owned by the current user. If your table has enough data, a user with 10 habits might get 0 returned, because all 10 were beyond the pagination cutoff.
The fix: Manually add a GSI (Global Secondary Index) on the owner field in your Amplify schema:
// amplify/data/resource.ts
const schema = a.schema({
Habit: a
.model({
name: a.string().required(),
// ...other fields
})
.authorization((allow) => [allow.owner()])
.secondaryIndexes((index) => [
index("owner") // <-- this is what saves you
]),
});
With this index, DynamoDB queries by owner first - only your records are touched, pagination works correctly, and the query is O(your data) instead of O(everyone's data).
This is not mentioned prominently in the Amplify docs. You'll only discover it when a user reports missing data or you notice your list queries returning fewer items than expected. Add the index from day one.
2. Optimistic UI is table stakes for a habit tracker
Nobody wants to wait 300ms for a checkbox to visually toggle. TanStack Query makes this straightforward:
const mutation = useMutation({
mutationFn: createCompletion,
onMutate: async (newCompletion) => {
await queryClient.cancelQueries({ queryKey: queryKeys.completions(date) });
const previous = queryClient.getQueryData(queryKeys.completions(date));
queryClient.setQueryData(queryKeys.completions(date), (old) => [...old, newCompletion]);
return { previous };
},
onError: (_, __, context) => {
queryClient.setQueryData(queryKeys.completions(date), context.previous);
},
});
The key is a centralized queryKeys.ts file. Once you have more than 3 or 4 query types, ad-hoc key strings become a maintenance headache.
3. Scheduling logic is harder than it looks
Supporting "every day", "weekdays only", "every 3 days", and "custom weekly days" means your calendar heatmap needs to know which days a habit was supposed to run before it can color them correctly.
All of that lives in my dateUtils.ts and is the most-tested part of the app. If you're building something similar - write tests for your date logic first.
4. Pre-rendering beats SSR for static landing pages
The app is a fully authenticated SPA, but the landing page needs to be crawlable by Google. Instead of adding SSR complexity, I use Puppeteer in a prerender script.
The static HTML files get deployed alongside the SPA. Google sees real content; users get the React app. Simple.
5. Multi-language hreflang is fiddly but important
Supporting Ukrainian, Russian, and English means:
Separate pre-rendered pages at /, /uk, /ru
hreflang tags in index.html
Sitemap entries for all three
Language-specific H1s (Google reads them for locale ranking)
The hreflang validator is your friend.
What I'd do differently
- Start with auth earlier. I built a lot of UI before wiring up Cognito and had to redo state management.
- Use a Lambda for bulk ops from day one. I added it later and had to migrate data.
- Write date utils tests before the UI. Every bug I had was in scheduling logic.
If you track habits or want to try a clean, ad-free tracker: bikaizen.com
Happy to answer questions about any part of the stack.



Top comments (0)