Dmitry Isaenko

@dmitry-isaenko.bsky.social

Full-Stack Developer | Laravel & Vue.js | Building LaraFoundry.com - a modular SaaS engine battle-tested with Kohana.io (next-gen CRM/ERP) | Sharing architecture, code & lessons learned

Three things shipped this week, same move: the core leaves a seam, the host or a paid add-on fills it. - admin monetization slots a billing add-on takes over - an SEO kit for an Inertia SPA, no SSR daemon - a dismissible onboarding checklist open, under Pest. #larafoundry

LaraFoundry: a host extends the core's SEO sitemap and onboarding checklist through the same addProvider seam, with no edit to the core package.

Building a reusable SaaS core and a real host app at the same time. The host's quiet bugs (a scaffold shadowing a core contract, a prop wrapped in data) keep pushing fixes back into the core: config-driven OAuth, per-user dates, a killed testbench trap. #larafoundry

LaraFoundry core: a class_exists guard that auto-registers community OAuth drivers like Apple and Microsoft only when they are actually installed, so the package can support them without taking a hard dependency on them.

Most apps hardcode the "admin account under attack" alert to one channel, token in the code. In LaraFoundry it's an event. The core fires one signal for a failed admin password, OTP or PIN and ships email. Any other channel is a host listener. Zero core changes. 774 Pest tests. #larafoundry

LaraFoundry admin-access alert. A host listener subscribes to the AdminAccessAttemptFailed event and sends a Telegram message for a failed super-admin login, checking the shared alert policy first. The core ships only email; this channel is host code, no package change.

A validation rule that fails OPEN: In Laravel, where('company_id', null) becomes whereNull(), and global role templates have company_id null. So an exists() meant to lock you to your company can match every shared template. Fail closed: coalesce to an impossible id. #Laravel #PHP

LaraFoundry: a Laravel exists validation rule made fail-closed so a null company id cannot fall through to whereNull and match global role templates, scoping the role strictly to the active company.

Most SaaS affiliate programs are one hardcoded scheme. 20% recurring, take it or leave it. I built LaraFoundry's as a configurable engine: three axes (who becomes a partner, which payments pay, how commission is figured). Any combination. And turning it on touches zero host code. #Laravel #PHP

LaraFoundry affiliate program: the free core's CompanyCreated event and the billing add-on's CompanyPaymentProcessed event, with the add-on registering AttributeReferral and AccrueAffiliateCommission as listeners, so a partner program plugs in without any host application code.LaraFoundry affiliate config: three orthogonal axes, eligibility (auto, self_serve or admin), commission trigger (first_payment, recurring with a window, or lifetime), and basis (per_plan, percent or fixed), so a host chooses its own affiliate model instead of one hardcoded scheme.

Building a big project solo, I almost wasted weeks on the most "professional" thing: writing the giant upfront doc. Every route, method, test, rule. One map so nothing collides. That is Big Design Up Front. Teams dropped it on purpose. Here is what they do instead.

A PHP interface from the LaraFoundry SaaS engine showing a frozen seam. The PaymentGatewayManager interface declares a single charge method taking Money and Customer and returning a ChargeResult, with comments explaining that the shape is locked for everyone who depends on it while the implementation stays free to change.Three PHP interfaces from the LaraFoundry SaaS engine presented as frozen seams. EntitlementResolver connects the billing add-on to the free core, MenuProviderInterface connects navigation to the app, and DashboardWidgetProvider connects the dashboard to its widgets, with a comment noting the paid add-on was built entirely against these contracts without changing the core.

Most Laravel SaaS starters are Stripe-only. Stripe reaches ~46 countries, and my market isn't one. So I built billing for LaraFoundry around one gateway contract. Stripe and Paddle are just drivers behind it. Plans carry a real price per currency, not one USD with a converter. #Laravel #PHP

LaraFoundry billing: the open-core PaymentGatewayInterface contract with subscribe, subscriptionStatus and verifyWebhook methods, and below it the paid add-on registering Stripe and Paddle as drivers via the manager extend() method, so call sites stay provider-agnostic.LaraFoundry billing: a plan's priceFor(period, currency) method returning a real price in EUR and in PLN as minor units, showing that plans in LaraFoundry are priced per currency rather than converted from a single USD figure.

SaaS access is two independent questions: RBAC = who in the company may. Entitlement = what their plan paid for. A manager can have production.view and still sit on a plan that never bought that module. So I gate routes by both: can:production.view + entitlement:production.module #Laravel #PHP

LaraFoundry app: a Laravel route for /production guarded by two middleware, can:production.view (RBAC permission, who may) and entitlement:production.module (LaraFoundry billing add-on, what the plan paid for).LaraFoundry billing add-on: PHP class PlanEntitlementResolver implementing the core EntitlementResolver. Its allows() method returns true when billing is off, otherwise loads the tenant plan and returns whether the plan features include the requested feature. Fail-closed, never throws.

tagged v0.10.0 of LaraFoundry: the admin companies console. second screen of the operator panel. headline feature: an operator can suspend a whole tenant now. the hard part wasn't blocking it. it was not locking out members who belong to OTHER companies 👇

LaraFoundry admin companies console: the tenancy middleware promoting a user's next un-blocked company instead of locking them out when their active company is suspended.

tagged v0.9.0 of LaraFoundry: billing. honest headline: i shipped the billing SEAM, not billing. the free core now has the whole shape of a subscription system, gateway contract, driver manager, real access gate, and it cannot take a single cent. on purpose 👇

LaraFoundry billing seam: the null payment gateway throwing instead of returning the donor's hardcoded success

Did you know? On GitHub, a repo named exactly like your username becomes your profile page. Its README is what people see when they open your account. Then: archived repos still show (go private or delete), and you can pin your best repos to the top. Link to mine in the replies.

Bild

tagged v0.8.0 of LaraFoundry: the file & media layer. that closes phase 2. no dramatic bug this time. just a habit i had to unlearn, one that worked fine in the original CRM and breaks the moment the code has to be reusable: saving files with public_path() 👇

tagged v0.7.0 of LaraFoundry: navigation + the first operator console screen. while rebuilding it i found a hole in the original CRM that had been live the whole time: any admin could log in as any user, with no record of it 👇

tagged v0.6.0 of LaraFoundry: multilanguage. a language switcher and a second language. the most boring thing in the backlog. you have built it a hundred times. it shipped two real bugs, and both lived where two systems meet 👇

tagged v0.5.0 of LaraFoundry: the activity log. i didn't invent an audit log. it's spatie under the hood. what i extracted is the context around it: device, IP, route, geo on every entry, plus a super-admin viewer. then the review caught it logging the wrong thing 👇

Bild

tagged v0.4.0 of LaraFoundry: roles & permissions. self-written RBAC (not Spatie), tenant-scoped from the ground up, lifted from my live CRM. roles, per-user grants and revokes, default roles cloned into every new company. then my code review found a privilege-escalation hole I'd have shipped 👇

tagged v0.3.0 of LaraFoundry: multi-tenancy. one db, many tenants, row-level isolation. teams or personal mode behind a config switch. ran my code review, saw "8 of 12 handled", almost shipped. ran it again. the 2nd pass found 2 security holes the 1st missed 👇

Bild