

PTLog
SaaS · PWAA multi-tenant management platform for gyms, with a member PWA.
TECHNOLOGY
From PTLog, ClinicPulse, HuntLine IQ and PeakUp: tenant isolation in RLS, authorisation tests in SQL, hosting on Vercel or Cloudflare.
SHORT ANSWER
AMG Software Studio builds SaaS products with a Next.js front end and Supabase for the database, auth and Edge Functions, leaving tenant isolation to Postgres RLS rather than application filters. ClinicPulse’s ten tables and 34 RLS policies, PTLog’s tenant claims written into the session JWT and HuntLine IQ’s 28 migrations and 25 Edge Functions are examples of the approach.
Projects that use both need to be kept apart from projects that use only one, because the stack is not the same everywhere:
The distinction matters because some of the RLS and testing practice below comes from ozelokul.com and RichlyMe. Those are evidence for the Supabase side, not the Next.js side. For tenant resolution, cron routes and hosting on the Next.js side, the examples are PTLog, ClinicPulse, HuntLine IQ and our own site.
The first question in a multi-tenant SaaS is where a request gets assigned to a tenant. In our projects the answer is: the tenant goes into the session, and the database enforces isolation.
In PTLog, every gym is a tenant on one codebase. Next.js middleware resolves gym.ptlog.dev or a custom domain to the tenant, and Supabase’s custom_access_token_hook writes tenant, role and permission claims into the session JWT. A branding engine serves a separate PWA manifest and sharp-generated icons per hostname, so members add the gym’s own app to their home screen.
In ClinicPulse, tenant isolation lives in ten tables and 34 RLS policies; nothing relies on application code remembering where clinic_id = …. Adding a clinic is one row, with no separate deployment. Sensitive fields are encrypted with AES-256-GCM, and the app refuses to start without the key.
In ozelokul.com, the tenant is the brand and the campus is a foreign-keyed scope inside it. Roles are 56 permission keys rather than an enum; every check goes through private.has_perm(), and membership and scope are written in a single transaction.
Reading a policy and deciding it looks right is not a test. Our tests switch to the client’s role and actually attempt the forbidden operation:
BEGIN … ROLLBACK; test.as_user() sets the JWT claims and test.throws() checks the expected SQLSTATE, usually 42501. Structural tests assert "no table without RLS" and "anon can reach nothing", which also covers tables added later.authenticated, because a superuser bypasses RLS and everything passes for nothing. In a mutation test, removing campus scope from has_perm turned the suite red on 5 assertions.supabase test db, with CI on every push.pnpm dogrula gate.RLS protects rows, not columns. During the ozelokul.com authorisation audit we showed with a live attempt that a "users may update their own profile" policy opens the whole row: a parent account could set its own account_type to platform with a single PATCH request. Five places of the same kind were closed with column grants and a trigger. Parent contact details are masked in a view with case when contact_revealed.
The second trap is the policies themselves. Once EXECUTE was revoked on private-schema functions, every policy failed with permission denied; the fix was granting EXECUTE only on the 9 functions policies reference. A policy that read another RLS table failed with 42P17, infinite recursion, and that access moved into a SECURITY DEFINER function.
The third is the gap between screen and API. In RichlyMe, a blurred proof’s feed row still carried the owner’s name and a publication time to the second: the screen hid it, the API handed it out. Plan limits follow the same principle; in ozelokul.com a locked section never comes back from the query at all, because of private.section_visible().
Database tests do not cover server functions, so at ozelokul.com 80 mutation endpoints were also mapped to their gates and 30 went through live exploit attempts with real JWTs.
In RichlyMe, business rules live in SQL while auth and input validation sit in thin Edge Function shells: authenticate, validate, call one RPC. Nine of the 17 functions can only be called with the service role. Our Next.js projects follow a similar layout:
--no-verify-jwt; the other 23 verify the member’s token. Daily cron runs through /api/cron/* routes on the Next.js side.api/cron/reminders route, and the send endpoint is capped at ten requests a minute.x-peakup-cron header, and edge_cagri_sagligi(24) feeds the last 24 hours of 401s and 5xx errors to the panel’s system status card. A missing vault secret makes the cron job throw; nothing dies silently.FOR UPDATE SKIP LOCKED. Event, invite and credit are created in one database transaction, safe to retry with the same request id.We run products on both. HuntLine IQ’s Next.js 14 web app is live on Vercel, and the native mobile apps read the same Supabase data. In Viralance, media is served from Cloudflare R2 with Vercel as the fallback.
Our own site, amgsoftwarestudio.com, compiles its Next.js 16 App Router code with vinext and runs it as a single Cloudflare Worker. The Worker keeps public HTML from anonymous GET requests in the Cache API for 300 seconds and adds the deployment version id to the cache key, so each release starts with fresh keys and there is no purge step. In a small sample from one location on 31 October 2026, cached pages returned their first byte in 0.15–0.25 seconds. Instead of relying on Next.js image optimisation on Workers, images are generated at seven widths during the build and mapped with a custom loader.
On our own site, publishing is manual on purpose: GitHub Actions only validates types, lint, tests and the production build, and CI holds no Cloudflare deployment secret. For client projects, hosting is chosen during discovery, and the domain, servers, database and service accounts are opened in the client’s name.
Yes, as the second line. In ozelokul.com every table is born with RLS, and application checks are the second defence. Database tests do not cover server functions, so 80 mutation endpoints were mapped to their gates and 30 were tried live with real JWTs. Two of the four exploited findings had no RLS counterpart either, so a second line of defence was added for them.
Yes. At ozelokul.com the client had a finished design running entirely on mock data, about 53 routes and 50,000 lines. The brief was to build a production-grade backend underneath without touching a pixel. Every field of the mock data was mapped to the schema first, then loaded as a seed migration; there are 139 migrations today. Note that this project’s front end is Vite + TanStack Start, not Next.js.
Not when the product is built multi-tenant. In ClinicPulse a new clinic is one row, with no separate deployment. In PTLog every gym opens on the same codebase under its own subdomain or custom domain and gets its own branded PWA. Plan differences can live in data too, as plan_features rows in ozelokul.com, rather than in code.
It depends on the product. In HuntLine IQ, Stripe and RevenueCat webhooks run as Edge Functions; Kutylawsuit uses Stripe Checkout with webhook verification; EntMoment has Stripe and a weekly payout cron. At ozelokul.com there is no payment provider, by client decision: the flow runs on bank transfer, and admin approval marks the invoice paid, activates the subscription and writes the audit record in one transaction.
Yes, our own site runs that way. The Next.js 16 code behind amgsoftwarestudio.com is compiled with vinext through Vite into a single Worker bundle, with React Server Components and server-rendered HTML in the same bundle. Redirects and small API endpoints are resolved in the Worker entry file before the app sees the request. There is no dependency on paid Cloudflare Images; images are prepared at build time.
Yes, several of our projects work that way. HuntLine IQ’s Expo app uses the same data model and Supabase project as the web platform, with authorisation enforced by RLS in the database. PeakUp’s Next.js 16 panel shares the schema and the @peakup/shared business rules with the mobile app. In Karaman, the customer mobile app, the restaurant panel and the central admin panel share one Supabase project.
You do. The domain, servers, database and service accounts are opened in your name. At delivery we hand over the full source code, the design files and every credential.
No. We design the user screens and the admin panel screens for your project and build them from scratch, so the panel follows your workflow.
RELATED STUDIO NOTES


A multi-tenant management platform for gyms, with a member PWA.


A multi-tenant clinic management system.


Hunting movement insight that never exposes your exact location.


Fitness studio management and member tracking in one app.


A private-school, college and university discovery platform with a parent centre and school panels.


One selfie finds every photo you appear in.
It starts with an intro call. We define the scope and timeline together, then send you a written proposal.
hello@amgsoftwarestudio.com