Skip to content
Bùi Hữu Tiến
All projects
  • SaaS · Multi-tenant
  • B2B · Japan
  • 59 PRs merged

Benerio

Full-stack developer on a multi-tenant B2B SaaS for Japanese multi-location businesses: built core Google Business Profile features, the partner agency tool, and plan-bound account linking with per-location permissions, across 600+ commits and 59 merged pull requests.

Visit the live site (opens in a new tab)
Role
Full-stack
Team
~3 people
Timeline
06/2025 – 12/2025
Status
In production
  • 600+

    Commits (459 code)

  • 59

    Pull requests merged

  • ~60

    Client feedback tickets

  • 144

    RLS policies in the platform

  • Next.js
  • TypeScript
  • Supabase
  • PostgreSQL
  • Tailwind CSS
  • Vercel

Context

Benerio is a multi-tenant B2B SaaS for the Japanese market that lets businesses with many locations manage their Google Business Profile listings and Instagram / Facebook / Threads accounts from one dashboard, with AI built in. Services run on the platform as separate modules: GBP Manager, SNS Manager, a tool that lets partner agencies manage many clients, and a few industry-specific modules.

It is built with Next.js 14 (App Router), TypeScript and Supabase, deployed on Vercel, with a Japanese interface and business content in Japanese, English and Chinese. It is a large codebase that about fifteen people have contributed to since late 2024; while I was on it, around three developers were active at the same time. I worked on Benerio at Protean Studios from June to December 2025.

Problem

  • Multi-location businesses had to post, reply to reviews, update details and track metrics on every Google profile and every social account separately.
  • Agencies managing many clients needed to do the same across all their clients, but only on the locations assigned to them.
  • The platform had to isolate each company's data, enforce permissions per company and per location, and charge by plan and usage.

My Responsibility

I worked full stack across every layer: PostgreSQL migrations and Row Level Security, server-side API routes and React interfaces. Four main areas:

  • GBP Manager: business information editing, review management, scheduled posts, analytics and PDF reports, AI translation, sync jobs.
  • Partner agency tool: client management, GBP posts with drafts and scheduling, failed-post handling, media management.
  • Plans and usage: Google accounts bound to plans, per-location permissions, RLS for agency-supported locations.
  • Google sync: the OAuth flow for linking and unlinking GBP, plus syncing posts and reviews.

In 2025: 619 commits (459 code commits), 59 merged pull requests and about 60 feedback tickets from Japanese clients — through feature branches, pull requests and code review, communicating in English and Japanese.

Constraints

  • A large shared codebase (~185,000 lines of TypeScript): every change had to follow the existing module architecture and migration process.
  • Isolation had to hold even if app code was wrong: one company must never see another company's data.
  • Agencies act on behalf of clients: access has to cross tenants, but only for the locations they support.
  • External APIs: Google OAuth tokens expire, APIs have quotas and rate limits, and data on Google can be edited or deleted outside the system.
  • Japanese clients reported issues as numbered tickets; three environments (local, staging, production), with a manual confirmation step for production deploys.

Architecture

  • Next.js 14 on Vercel is the only middle layer: users and cron jobs both pass through it before touching Supabase or the Google, Meta and LLM provider APIs.
  • Services are plugins (built by the platform team): each module declares its routes in a manifest.json, and one catch-all route loads a module only when that service is enabled for the company.
  • Data is hierarchical — Company → Location → User; one user can belong to several companies with different permission groups. Integration settings live in a service catalogue and a table of services enabled per company.
  • Protection lives in the database: 71 tables, 144 RLS policies and 73 PostgreSQL functions isolate data by company and permission group.
  • Background work: 10 Vercel cron jobs — scheduled posts every 5 minutes, daily and weekly GBP and social metrics sync.

In the diagram, the highlighted blocks are the parts I built directly; the rest is the platform the team built.

Diagram: owners, staff and agencies use a Next.js 14 app on Vercel whose service loader enables modules per company. Highlighted as built by me: the GBP Manager (business info, reviews, posts, analytics and PDF reports), the agency tool (drafts, schedules, media) and plans with per-location access and RLS. Vercel Cron triggers scheduled posts and sync; data lives in Supabase Postgres with 71 tables and 144 RLS policies; the modules talk to the Google Business Profile API, Meta and LLM providers.
Scroll sideways to see the whole diagram

Key Technical Decisions

Plans and usage live in the data model

  • Problem: linked Google and social accounts must count against the active plan, and features should only open for locations on a suitable plan.
  • Decision: an integrated_service_usage model that binds each linked account to the active plan; flows to link and unlink GBP accounts per plan; a per-location plan check before a feature runs; an RPC function that disconnects a service in a single call; a page that tracks AI usage.
  • Why: plan limits are enforced in data, not by whether the UI hides a button.
  • Trade-off: more checks whenever a feature opens, and unlinking has to clean up consistently.

Two permission layers: the UI for experience, the database for security

  • Decision: access granted per location for each user; a frontend permission hook that shows or hides sidebar items and actions; RLS in PostgreSQL as the real barrier.
  • Why: UI checks keep users from seeing actions they cannot take; RLS makes sure an app-level bug still cannot leak data.
  • Trade-off: both layers have to encode the same rules.

RLS for agency-supported locations

  • Problem: agencies need to work on their clients' locations — which means crossing tenants.
  • Decision: RLS policies that grant access only to supported locations, together with an agency client-management page and an access-transfer flow.
  • Why: the agency model keeps the isolation guarantee at the database level.

Syncing posts with Google: upsert and clean up deletions

  • Problem: posts can be created, edited or deleted directly on Google, outside the system.
  • Decision: a sync job that upserts posts from Google and deletes local posts that no longer exist on Google, tied to the client's plan.
  • Why: reruns never create duplicates, and no "ghost" posts linger in the system.

A clear post lifecycle for agencies

  • Decision: GBP posts move through draft → scheduled → published / CANCELLED / failed, with failed-post handling, media stored in Supabase Storage, and CRUD for keywords, hashtags and post templates.
  • Why: agencies prepare content ahead for many clients and need to see at once which posts did not go out.

Fit the existing architecture

  • Decision: move the GBP APIs into the service module, following the plugin architecture; route every database change through migrations and regenerate the TypeScript types from the schema.
  • Why: in a codebase many people change, new features must not break core routing or let DB and code types drift apart.

Trade-offs

  • Two permission layers (UI + RLS): good experience and safety, at the cost of rules living in two places.
  • Cron-based sync instead of webhooks: simple and easy to rerun, at the cost of data lagging until the next run.
  • Plan limits enforced in data: more reliable, at the cost of an extra check on every action.
  • A large team codebase: following the existing architecture and review process is slower than working alone, in return for a consistent system.

Implementation Highlights

  • GBP business information: sub-categories, address, service area and opening hours; review management with listing, filtering, replies, reply deletion and review sync.
  • GBP posts: a post screen with image upload and cancelling or deleting scheduled posts; AI translation of content.
  • Analytics and reports: top search keywords, review counts, star rating and map pin; report settings and PDF export (logo, keyword table).
  • Shared platform pieces: a company and location picker, file uploads to Supabase Storage, the integrated-service detail screen, and shared support modules for the agency tools.
  • Google integration: the GBP OAuth flow, account linking and unlinking, and logging of Google API errors.
  • Database: my own migrations (new columns, dropped unique constraints, RLS, RPC functions) and regenerated TypeScript types.
  • Working with clients: about 60 feedback tickets from Japanese clients; changes revised through code review.

Result / Impact

  • Delivered four feature areas in 2025: GBP Manager, the partner agency tool, plans with per-location permissions, and Google sync.
  • 619 commits (459 code commits), about +116,000 / −45,000 lines, 413 new files and 59 merged pull requests.
  • Resolved about 60 feedback tickets directly from Japanese clients.

What I Learned

  • Isolation between customers belongs in the database; app-level checks only serve the experience.
  • Plans, usage metering and feature toggles are data-model problems before they are UI problems.
  • Syncing with external APIs needs upserts, cleanup of deleted data, and handling of expiring tokens and rate limits.
  • In a large team, following the architecture and process (modules, migrations, review) matters more than a clever solution of my own.