Freelance
Korner
- iOS
- Android
- Fullstack Engineer
Legal tech platform that connects people with lawyers for 30-minute live consultations, booked and paid in the app. Korner connects clients in Indonesia with lawyers for paid 30-minute live consultations. A client picks an approved lawyer and a slot, describes the case in writing and in a voice note, and pays online. The chat room for a slot opens 5 minutes before it starts and closes itself when the time is up. I was the only developer, hired as a freelance consultant. I recommended the stack and the hosted services. The client decided. The backend started from my own ack-nestjs-boilerplate. I built the NestJS API, the Next.js admin dashboard, and the Flutter app for iOS and Android. The Flutter app shipped to the App Store and Google Play.
- Where
- Freelance project for Korner, not employment
- Role
- Fullstack Engineer, the only developer. I built the NestJS API, the Next.js admin dashboard, and the Flutter app for iOS and Android.
- Dates
- Oct 2023 – Apr 2024
- Platforms
- iOS and Android (App Store and Google Play)
- Stack
- NestJS 10, TypeScript 5.4, MongoDB, Flutter 3.16, Next.js 14, Twilio, Xendit, AWS
- per consultation
- 30 min
- close paths
- 3
- invoice expiry
- 15 min
- iOS and Android from one Flutter app
- Cross-platform
Infra
01Context and my role
I was the only developer, hired as a freelance consultant. I coordinated the project directly with the client. I recommended the stack and the hosted services. The client decided. The backend started from my own ack-nestjs-boilerplate. The Flutter app shipped to the App Store and Google Play.
02What it does
Lawyer publish
A lawyer submits a profile, ID card and credentials. An admin sets publish to on review, approve, reject, inactive or cancel. Only approve can be booked.
Schedules
Lawyers set weekly availability in 30-minute slots between 08:00 and 20:00 on lawyer-schedule.
Booking
A client books up to 30 days ahead with a written description, an optional voice note of up to 5 minutes and up to 3 PDF documents. A slot cannot be double-booked, and a client can hold at most 5 unpaid bookings at once.
Payment
A Xendit invoice per booking that expires after 15 minutes. The app opens Xendit's hosted invoice page. Lawyers are paid out through Xendit.
Consultation room
Real-time text chat between client and lawyer, a countdown in the app and an alert 10 minutes before the end. The voice note and documents are playable and viewable in the room.
Reminders
WhatsApp 15 minutes before the session and push 5 minutes before.
After the session
The client leaves a review.
Account
Sign-in by password, Google, Apple or WhatsApp OTP, password reset, terms (tnc) and privacy policy.
Admin dashboard
A Next.js admin that approves lawyers, reviews lawyers, and lists transactions.
Language
English and Indonesian.
03How it works
Booking and payment
- Creating a booking is one MongoDB transaction: the consultation record, the S3 uploads and the Xendit invoice succeed or fail together.
- Status flow: waiting for payment, then reserve, then completed, or cancel by client, or canceled.
- The Xendit webhook is checked with a signature before it changes anything. A paid webhook sets reserve and creates the chat room.
A session that closes itself, in three layers
- Chat service timer: when the room is created, the conversation on Twilio Conversations gets a close timer equal to the minutes left until the slot ends.
- Webhook: when the chat service closes the conversation, its webhook marks the room closed and the consultation completed, inside a transaction.
- Cron sweep: a NestJS cron job runs every 10 minutes and closes any expired consultation the webhook missed. An environment flag can switch it off.
- Users can open the room only from 5 minutes before the start until the end.
As client
Voice notes and files
- Recorded in the app as AAC (m4a) with a waveform, validated on the server for format and declared duration, stored on S3 under randomised paths, and linked to the consultation.
Chat
- The backend creates one conversation per consultation on Twilio Conversations, adds the lawyer and the client, and issues their access tokens. The app connects with the Twilio Conversations SDK. New messages trigger a push notification through the webhook.
Structure
- 19 domain modules (consultation, district, email, lawyer-schedule, privacy-policy, privacy-policy-history, province, publish, push-notification, regency, reset-password, role, setting, tnc, tnc-history, user, verification, village, whatsapp) on 15 shared modules from the boilerplate (API key, auth, AWS, database, logging, docs, errors, files, helpers, i18n, pagination, policy, request, response, health).
- 4 roles (super admin, admin, lawyer, client). Each module has separate controllers for client, lawyer, admin and incoming webhooks, and CASL policies check every route.
- Mobile app: MVVM with Provider view models and get_it for dependency injection, 16 feature areas (administrative, agreement, auth, chat, consultation, home, publish, reset_password, review, room, schedule, setting, sign_up, splash, user, verification).
As lawyer
04Decisions and trade-offs
Engagement
Next.js admin, three actions
- Why
- I recommended Next.js because I could use an existing starter and build the dashboard quickly. The surface is approve lawyer, review lawyer, and list transactions.
One Flutter app for both stores
- Why
- I recommended Flutter so one codebase covers iOS and Android. The app shipped to the App Store and Google Play.
Chat and payments
Hosted chat on Twilio Conversations
- Why
- I recommended hosted chat because building chat from scratch takes time and the client's timeline was short. The client chose Twilio Conversations. The API only manages rooms and tokens.
Xendit invoices and payouts
- Why
- I recommended Xendit so admin does not have to record transactions or transfer money by hand. Lawyers can take payouts themselves. The client chose Xendit.
Reminders
- Why
- I recommended reminders so the client is ready before the slot starts. WhatsApp is one path. Email and push carry them too.
- Cost
- Each WhatsApp message is billed. Email and push are not billed per transaction and have large quotas.
05Testing and delivery
- Tests: 18 Jest unit test files on the shared services from my
ack-nestjs-boilerplate, with a 100% coverage threshold on those services. The Korner domain modules have no automated tests. - CI/CD (GitHub Actions): on main and staging, a multi-architecture Docker Buildx image is pushed to AWS ECR, then an SSH step pulls it on the server and restarts the container. Production and staging have separate jobs. Lint and test workflows on Node.js LTS, and a release workflow that tags a GitHub release.
- Local quality gates: Husky, ESLint, Prettier, cspell, a dead-code check.
- Operations: data seeding and migrations from a command-line tool, Swagger docs, health checks, Sentry, rotating Winston logs.
06Stack
- Backend
Node.js 20
TypeScript 5.4
NestJS 10
Mongoose 8 (MongoDB, transactions)
NestJS Schedule (cron)
Swagger
NestJS i18n
- Auth and access
Passport JWT (access and refresh)
Google sign-in
Apple sign-in
CASL
- Chat and messaging
Twilio Conversations
Twilio WhatsApp
OneSignal (push)
AWS SES (email)
- Payments
Xendit invoices, webhooks and payouts
- Files
AWS S3
- Security and ops
NestJS Throttler
Sentry
- Mobile
Flutter 3.16
Dart 3.2
Sentry
- Web dashboard
Next.js 14
- Testing
Jest
- Delivery
Docker (multi-arch Buildx)
GitHub Actions
AWS ECR