Kadence International, full-time
Kadence K-OINS
- iOS
- Android
- App Store and Google Play
Kadence's online survey and panel app. Panelists sign up on their phone, are matched to surveys by demographics, answer them, earn points and cash the points out to a bank account or e-wallet. Behind the app is a NestJS microservices backend that K-OINS shares with Kadence Kepo. K-OINS runs on 4 of its services: the user service, built first during Kepo, and the survey, point and contact services, built for K-OINS. Each service has its own MongoDB database, and the services talk to each other only through RabbitMQ. The mobile app and the web front end reach the services over REST with a JWT.
- Where
- Kadence International, full-time employment
- Role
- First engineer on the team. I interviewed the internal users and benchmarked other apps, then designed and built K-OINS from scratch: its survey, point and contact services, the whole Flutter app and the infrastructure, CI/CD included. K-OINS also runs on the user service I built earlier for Kepo. A teammate built the web front end. I led the project.
- Team
- Me, 1 web front-end engineer, 1 designer and 2 interns (1 engineering, 1 design). I led the design direction, the technical direction and the interns. I reviewed the team's code and coordinated with project managers, QA and operations.
- Dates
- Jan 2022 – Nov 2024
- Platforms
- iOS and Android (App Store and Google Play), in Indonesia only
- Stack
- NestJS 10, TypeScript 5.2, MongoDB (Mongoose 7), RabbitMQ, Flutter, AWS
- microservices, database per service
- 4
- question types
- 11
- logic outcomes: jump, disqualify, complete
- 3
- iOS and Android from one Flutter app
- Cross-platform
Services and how they talk
01Context and my role
I designed the service split, built the survey, point and contact services, the Flutter app and the infrastructure, and owned the CI/CD. The user service, which K-OINS also runs on, I had built earlier for Kepo. A teammate built the web front end.
I asked for a moodboard first, then recommended the fundamentals: typography, color, and the rest.
Once those were agreed, the designer built the components and the design system, then wireframes, then high-fidelity screens.
We skipped the prototype and the mockup because time was short, the team was small, and other work still had to be handled.
Each morning we held a short standup, then worked that day's cards on a basic Trello board, with no Jira. When a topic needed to continue, it stayed with the person it concerned. Everyone else went back to the work.
- Jan 2022Project starts
I interviewed the internal users and benchmarked other apps, then designed K-OINS and built its survey, point and contact services and its Flutter app from scratch.
- Nov 2024Work ends
The work ended when I left Kadence.
02Why Kadence built it in-house
A vendor built the panel app before K-OINS. It cost IDR 300M to build, plus IDR 20M a month for maintenance, plus IDR 20–30M a month for servers and services, which the vendor fee did not include. The vendor's code was very messy and the result unsatisfying: after delivery the app could not be used at all, so the IDR 300M was wasted. The vendor's app was rated 2 on the App Store and 1 on Google Play.
Built in-house, K-OINS costs Kadence only the IDR 20–30M a month for servers and services (team salaries aside). The IDR 300M build and the IDR 20M monthly maintenance fee are gone, Kadence owns the source code, and the team sets and follows its own standards. K-OINS is rated 4.2 on the App Store and 3.6 on Google Play, with 18k registered panelists and about 2k (11%) active a month. A survey is always running: 2–3 surveys a week.
03What it does
Panel sign-up
Password and Google sign-in, OTP verification by SMS, email or WhatsApp, ID-card verification, referrals, advance sign-up, and a region picker with 4 levels (province down to sub-district).
Survey engine
Surveys with a lifecycle (draft, test, active, closed), categories (normal, screening, consumption, ownership), priority surveys with a cap on how many can be active, version history, and import of surveys and questions from Excel.
11 question types
Single choice, multiple choice, open text, grid single, grid multiple, grid text, grid number, date, info text, rank and number. Each has display options: single choice as radio, select or autocomplete; rank as drag-and-drop; text as short, medium, long or email.
Survey logic
JUMP_TO,DISQUALIFIEDandCOMPLETEDactions, run asPREorPOSThooks on a question, with conditions on text answers (equal, not equal, in, not in) and number answers (equal, not equal, less than, more than, or equal). Response-based filters ("is answered", "not answered").Distribution
Open surveys, or surveys targeted by criteria (gender, age range, marital status, education, location by regency or province, or a hand-picked list of users). Quotas stop new answers once full; one response per user; GPS captured with each response; targeted push notifications when a survey goes live.
Progress and export
Response status (on progress, completed, disqualified), progress counts on the panelist's survey list, and a raw-data export to Excel that is uploaded to S3 and can be emailed.
Points and payouts
Points are earned from surveys, referrals, advance sign-up and direct grants, and burned through withdrawals. Survey points are granted automatically or after an admin approves each response. Payouts go to a bank account or e-wallet through Xendit, and a Xendit callback reports the result. Withdrawals total up to IDR 5M a month, and one panelist can withdraw at most IDR 300K a month.
Notifications and legal
Email, SMS, WhatsApp and push; terms and privacy policy with version history; contact-us; weekly report emails.
04How it works
Four services, one database each
K-OINS runs on 4 services of Kadence's NestJS microservices backend, which it shares with Kepo. I built the user service (sign-up and sign-in) first, during Kepo, then the survey, point and contact services for K-OINS. The user, point and contact services are shared with Kepo, and the survey service is K-OINS's own.
I designed K-OINS to grow until it could compete with SurveyToGo. At the start we did not know how many surveys could run at once. I prepared the code so that when the volume came we could scale horizontally and add capacity. The same applies to every service, which is why I split K-OINS by role into the Survey service, Point service and Contact service, next to the User service, rather than building one monolith.
- User service
- Accounts, sign-in, OTP, password reset, ID-card check, referrals, profile history, regions, weekly report, user export
- Survey service
- Clients and research projects, surveys (status, criteria, quota, history), questions and display, the logic engine, respondents and answers, raw-data export
- Point service
- Balances, earn and burn transactions, bank and e-wallet accounts, Xendit payouts and callbacks, weekly report
- Contact service
- Email, SMS, WhatsApp and push delivery, terms and privacy policy with versions, contact-us
- Database per service: each service has its own MongoDB database and owns its data. Other services keep only IDs.
- RabbitMQ between the services, and nothing else: every call from one service to another goes through RabbitMQ. When a service needs data another one owns, such as a user's profile, it sends a request and waits for the reply over RabbitMQ (request-reply). Notifications are asynchronous: the request that triggers one does not wait for it, and RabbitMQ handles its retries and timeouts. The survey service sends to user, point and contact; user to contact and point; point to contact; contact to user.
- REST only for the clients: the mobile app and the web front end call the services over REST with a JWT. Xendit calls the point service back through its webhook.
- Stateless identity: the panelist's profile and eligibility (terms accepted, minimum age, verified phone, ID card, advance sign-up) travel inside the JWT, so every service checks the token itself without calling the user service. The token payload can be AES-encrypted.
- Business rules as guards: quota, survey status, date window, criteria, category and one-response-per-user each live in their own NestJS guard, so a controller reads as a list of preconditions.
- CASL permissions on top of roles (super admin, admin, user); routes grouped as public, auth, user and admin.
- A cap on answers: answers are rate-limited to 100 per second so the small servers do not fall over. Each answer runs its question's PRE and POST logic.
- Small infrastructure: the user and survey services run 2 instances each behind 1 load balancer. The point and contact services run 1 instance each, with no load balancer.
- Operations: Mongoose transactions for exports and money flows; offset pagination; weekly cron jobs for report emails; rate limiting, Helmet, Swagger docs and health checks; English and Indonesian messages.
Sign up and sign in
How a survey is made
How a survey is distributed
How each question is answered
One panelist, one survey
From a completed survey to the payout
Mobile app
K-OINS needed one codebase for iOS and Android, so I built the app in Flutter. People download the app from the App Store and Google Play. We also built a simple landing page for marketing.
- Flutter with MVVM: view models on Provider (
ChangeNotifier), Dio for HTTP with one client per backend service. - 22 feature modules: the question screen renders all 11 question types and their display options.
- Device features: OneSignal push (with an iOS notification service extension), secure storage for tokens, geolocation, camera and image picker for ID cards, a web view, and English and Indonesian localisation.
- Three flavors (development, staging, production). iOS builds are obfuscated; Android release builds are split per CPU architecture.
05Decisions and trade-offs
Business
Built in-house, not with the vendor
- Why
- The vendor's app could not be used after delivery, so the IDR 300M build was wasted. In-house, that build fee and the IDR 20M monthly maintenance are gone. Kadence owns the source code and pays only the IDR 20–30M a month for servers and services, team salaries aside.
Admin approval as a point mode
- Why
- Surveys that need quality checks hold points until an admin approves the response. The others pay out automatically.
Infrastructure
Four services by role, not one monolith
- Why
- I designed K-OINS to grow until it could compete with SurveyToGo. At the start we did not know how many surveys could run at once. I prepared the code so that when the volume came we could scale horizontally and add capacity. The same applies to every service, so I split K-OINS by role into the Survey service, Point service and Contact service, next to the User service from Kepo.
One database per service
- Why
- Each service has its own MongoDB database and owns its data.
- Cost
- Other services keep only IDs.
RabbitMQ between the services
- Why
- We wanted reliable services that produce and consume events easily. A fetch of another service's data is a request-reply over RabbitMQ, and a notification never holds up the request that sent it. A service that is down does not lose its messages: they wait in its queue until it is back. Services know only the broker, not each other's addresses, so a second instance of the user or survey service takes work from the same queue.
- Cost
- One more piece of infrastructure to run, and a request-reply still waits for the other service's answer.
Answers capped at 100 per second
- Why
- The servers are small, and each answer runs its question's PRE and POST logic, so the cap keeps them from falling over.
- Cost
- Answers stop at 100 per second. The cap is configured, not a measured peak.
Other
Eligibility in the token
- Why
- Fewer calls to the user service on every survey request.
- Cost
- That a change to a panelist's profile applies only after the next token refresh.
One Flutter codebase for iOS and Android
- Why
- K-OINS needed one codebase for both stores, so I built the app in Flutter. People download it from the App Store and Google Play.
06Testing and delivery
- Tests: Jest unit tests cover each service, with a 100% coverage threshold.
- CI/CD (GitHub Actions from the start): for staging and production, test and lint workflows, then a two-stage Docker Buildx build pushed to Docker Hub with version and commit tags, the runner's IP added to the AWS security group for the deploy, and an SSH step that pulls the image and restarts the containers on AWS EC2.
- Mobile builds: run by hand with script aliases per flavor.
- Local quality gates: Husky git hooks, ESLint.
CI/CD and where the services run
07Stack
- Backend
Node.js 18
TypeScript 5.2
NestJS 10
Mongoose 7 (MongoDB)
NestJS Schedule
NestJS i18n
Swagger
- Auth and access
JWT (Passport) with optional AES payload encryption
Google sign-in
- API keys
CASL
- Files and data
AWS S3
- SheetJS (xlsx) for Excel import and export
- Payments
Xendit payouts and callbacks
- Between services
RabbitMQ (request-reply and asynchronous messages)
- Messaging
SendGrid (email)
- SMS and WhatsApp gateways
OneSignal (push)
- Security
- Helmet
NestJS Throttler
- Mobile
Flutter 3.13+
Dart 2.19+
- Provider (MVVM)
- Dio
OneSignal
- flutter_secure_storage
- geolocator
- camera
- image_picker
- webview_flutter
- Testing
Jest 29
- Delivery
Docker
Docker Hub
GitHub Actions
AWS EC2
- a load balancer