Sign in

Dmitry Isaenko

@dmitry-isaenko.bsky.social
92 followers 154 following 349 posts

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

PostsRepliesMedia
Dmitry Isaenko @dmitry-isaenko.bsky.social · 15/07/2026
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.
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 14/07/2026
I built Krokq: a light task tracker and chat under one shell, with a built-in AI assistant. The chat feels like the messengers you already use, nothing to learn. Create a task by voice, or turn a chat message into one in a click. Installs as an app, 10 languages, free. #BuildInPublic #larafoundry
Krokq in use: a chat thread that looks like a normal messenger, with a message being turned into a task in one click, showing how Krokq moves between chat and tasks.Krokq in use: a chat thread that looks like a normal messenger, with a message being turned into a task in one click, showing how Krokq moves between chat and tasks.Krokq in use: a chat thread that looks like a normal messenger, with a message being turned into a task in one click, showing how Krokq moves between chat and tasks.Krokq in use
160
Dmitry Isaenko @dmitry-isaenko.bsky.social · 01/07/2026
Shipped AGENTS.md and CLAUDE.md inside the composer package, so a coding agent understands my Laravel engine the moment you require it. The rules ride next to the code: never edit the host, fail closed, config over hardcoded lists. A fresh chat reads them instead of guessing. #larafoundry #AI
The golden-rules section of AGENTS.md, a dev-context file shipped inside the LaraFoundry composer package so a coding agent reads how to extend the engine (never edit the host, contracts and bindings, fail-closed, config-driven registries) instead of guessing.
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 29/06/2026
Built a whole warehouse module on Kohana and the engine under it didn't change by one line. Products, stock, attributes, an importer, all my domain. A record goes company-private the moment I add use BelongsToTenant. The rest was already there. #larafoundry #BuildInPublic #Kohana
Kohana.io's Product model in the warehouse module, isolated per company by adding the BelongsToTenant trait from the LaraFoundry engine. The domain code (stock, attributes, package types) is the host app's; tenancy and isolation come from the core with no changes to it.
060
Dmitry Isaenko @dmitry-isaenko.bsky.social · 22/06/2026
The real test of a reusable core isn't the app you built it for. I dropped my Laravel SaaS core into a second product (Comentor app - real users, a mobile API, a year-old DB). The seams fought back: package migrations failed on MySQL while SQLite tests stayed green and hid it. #larafoundry #Comentor
LaraFoundry in a second app: a migration that adds the standard auth columns before the package's own migration runs, because the existing users table never had them. SQLite tests passed and hid the problem, only MySQL caught the failure.
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 22/06/2026
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.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 18/06/2026
Adding a second country to a CRM usually means if-country checks everywhere. My fix: a Country Profile per country - tax fields, banking, carriers, messengers, currency. Turn one on, the user picks what they use. Core stays neutral, host tightens it. #larafoundry #Laravel
A LaraFoundry-based CRM config showing a Ukraine Country Profile: currency, legal id fields with format rules, banking fields, phone messengers and delivery carriers. The app uses the profile to render only the fields relevant to the selected country.
130
Dmitry Isaenko @dmitry-isaenko.bsky.social · 16/06/2026
Kohana is a CRM I built. I pulled its foundations into a Laravel package. Now I'm rebuilding Kohana on top of that package, and that is the whole point. #Kohana #LaraFoundry
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 16/06/2026
I built a CRM, pulled its core into a Laravel package, then rebuilt the CRM on that package, the real test before it ships. Day one: a full SaaS shell. Then profile saves dropped every field but name and email, a scaffolded Fortify action had shadowed my package's. #larafoundry #Laravel
LaraFoundry host: the single scaffolded line in a Laravel app that shadowed the package's Fortify profile-update action, so saves persisted only name and email until the line was deleted.
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 15/06/2026
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.
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 12/06/2026
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.
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 11/06/2026
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.
220
Dmitry Isaenko @dmitry-isaenko.bsky.social · 10/06/2026
In LaraFoundry, the right to access and the right to be forgotten are the same shape: every module registers one exporter and one purger, and the flows never learn the modules. Erasure is a reversible soft-delete plus an idempotent cron, not a hard DELETE. Built in public. #larafoundry #Laravel
LaraFoundry GDPR seam: two mirrored contracts, ExportsUserDataProvider with exportFor() and PurgesUserData with purgeFor(), both keyed and ordered, so every module plugs the same way into data export and account erasure.LaraFoundry account erasure command: it selects accounts soft-deleted past the grace window, runs the purge registry inside a transaction to anonymise them, and stamps user_purged_at so the daily cron is idempotent and never reprocesses a purged row.
130
Dmitry Isaenko @dmitry-isaenko.bsky.social · 09/06/2026
I let the super-admin edit email templates straight from the database. The renderer never touches Blade or eval, just a single-pass {{token}} replace, so a stored template cannot execute code. No SSTI, no RCE by construction. New in LaraFoundry, built in public. #larafoundry #Laravel
LaraFoundry email template editor: the renderer substitutes {{variable}} tokens with a single-pass preg_replace_callback and never compiles Blade or evaluates PHP, so a template stored in the database cannot execute code.LaraFoundry settings module: a config registry declares each setting with its scope (app, company or user) and type, and only registered keys can be read or written, a fail-closed key-value store.
120
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
I shipped a support desk for LaraFoundry by deleting a dependency. The core used to lean on a third-party ticket package. Wrong for a reusable core, so I cut it and wrote the model myself. Less code, the status is derived from who replied, not a dropdown you forget to set. #larafoundry #Laravel
LaraFoundry support tickets. The status is derived from who replied instead of being chosen from a dropdown: a user reply sets wait-moderator, an operator reply sets wait-customer.LaraFoundry helpdesk routes. Support tickets sit behind auth only with no account-active gate, so a company suspended for billing can still reach support, while an operator-banned account stays logged out.
110
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
You probably don't need WebSockets for in-app notifications. A full notification centre in LaraFoundry: a bell, a paginated inbox, super-admin broadcasts. No Redis, no daemon, just polling and a database queue. Broadcasts queue and fan out in chunks, idempotent on retry. #larafoundry #Laravel
LaraFoundry notifications. A host app sends an in-app notification from its own domain event through NotificationService, using translation keys so the wording localises per recipient.LaraFoundry notifications. A super-admin broadcast fans out to its audience in queued, chunked database inserts, using insertOrIgnore so a retried job stays idempotent.
110
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
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.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
Added QR cross-device login to the LaraFoundry core. Pulled it from a production app. It worked, and it had 11 security holes. Plaintext token in the DB. A scanner that fetched any decoded URL. Session fixation. No rate limit. Closed all 11 before merge. v0.13.0. #Laravel #BuildInPublic
LaraFoundry core QR login: the sign-in token is SHA-256 hashed before it is stored, so the database never holds a usable code; verify matches by hash.LaraFoundry core QR scanner: a guard that validates the scanned URL by scheme, same origin and the exact verify path before issuing any request, closing the donor's blind-fetch SSRF.
250
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
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.
110
Dmitry Isaenko @dmitry-isaenko.bsky.social · 06/06/2026
Loved it! 😂 So accurate!
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 05/06/2026
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.
130
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
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.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
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
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
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.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
public_path() to save an upload works, until the code isn't one app on one server. it assumes files live in the deployable web root (redeploy wipes them), assumes local disk (no mkdir on S3), assumes the app owns the path. a core shipped to strangers gets to assume none of that.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
the hole: "log in as a user" for support. the original CRM's authorization check was one commented-out method. the package it used defaults to ALLOW when that method is missing. so any admin could impersonate anyone. no check on the target. no log it ever happened.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
bug two, and the review found this one, not me. the invite heading was $t('Join :company', { company }). looks right. but :company is Laravel syntax. that line is vue-i18n, whose syntax is {company}. so it rendered the literal "Join :company" and ignored the name. failed silently.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 03/06/2026
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 👇
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 03/06/2026
the fix is one rule: you can only hand out what you already hold. the actor's own permission set bounds every grant and role they assign. owners and super-admins hold everything, so nothing changes for them. a delegated member is capped at their own access. a regression test pins it shut.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 03/06/2026
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 👇
200
Dmitry Isaenko @dmitry-isaenko.bsky.social · 29/05/2026
Testing payments in LaraFoundry 3 test files. model logic, controller actions, edge cases. all Pest. #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
040
Dmitry Isaenko @dmitry-isaenko.bsky.social · 22/05/2026
Testing tickets in LaraFoundry feature tests, unit tests, policy tests. real HTTP, real DB. the policy works or the test fails. #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
031
Dmitry Isaenko @dmitry-isaenko.bsky.social · 21/05/2026
LaraFoundry auto-hides resolved tickets after 7 days: scopeExcludeOldResolved($query, $days = 7) no delete. no archive button. just a scope that filters them out. keeps the user interface clean without data loss. #Laravel #PHP #LaraFoundry #UX
040
Dmitry Isaenko @dmitry-isaenko.bsky.social · 20/05/2026
Admin ticket dashboard for LaraFoundry. Clean grid view, interactive stat cards, and dynamic filters. List view is up next! hashtag#BuildInPublic hashtag#Laravel hashtag#LaraFoundry hashtag#SaaS
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 17/05/2026
My first truly "useful yet useless" DIY project is ready! Built this for a relative who is a huge Harry Potter fan. ⚡🧙‍♂️ Link to the source code is in the first comment 👇 #3DPrinting #HarryPotter #Maker #BuildInPublic
160
Dmitry Isaenko @dmitry-isaenko.bsky.social · 15/05/2026
Testing notification segmentation in LaraFoundry Real HTTP. Real DB. The filter either works or it doesn't. #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
040
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/05/2026
Why `hasAccess()` checks both trial AND subscription in LaraFoundry? Simple. But it means a company on trial with an expired subscription still works. Trial takes priority. No double penalty for early adopters who explore before paying. #Laravel #LaraFoundry #SaaS #Design
060
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/05/2026
Testing ban cascade in LaraFoundry Real HTTP. No mocking. Behavior that matters: one user's ban affects another user's access. #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
040
Dmitry Isaenko @dmitry-isaenko.bsky.social · 06/05/2026
block_status is shared on every page in LaraFoundry Frontend can show warning banners ("3 days left"), disable features, or redirect. No extra API calls. middleware blocks. inertia warns. two mechanisms, two purposes. #InertiaJS #Vue #LaraFoundry #Laravel #Frontend
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 06/05/2026
Every authenticated request in LaraFoundry checks One action. Single source of truth. Shared with frontend via Inertia props. #Laravel #LaraFoundry #PHP #Architecture
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 05/05/2026
After these tweaks, the OrcaCube test was a success - everything fits like a glove. Any questions about the calibration process? Let's discuss!
OrcaCube test
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 01/05/2026
Testing admin user management in LaraFoundry Real HTTP requests. No mocking. behavior tests > implementation tests. #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
030
Dmitry Isaenko @dmitry-isaenko.bsky.social · 29/04/2026
"I can't see that button on my dashboard." LaraFoundry admin clicks "Follow". Now they're logged in as that user. Seeing exactly what they see. Click "Leave" - back to admin. No re-login. Requires 2FA. Logged in activity log. Frontend shows warning bar #Laravel #LaraFoundry #AdminPanel #DevTools
130
Dmitry Isaenko @dmitry-isaenko.bsky.social · 27/04/2026
Passwords in LaraFoundry controllers Eloquent Attribute mutator. Controller never calls Hash::make(). Create user, update user - doesn't matter. Hashing happens in one place. #Laravel #PHP #LaraFoundry #Security
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 23/04/2026
Testing 11 middlewares in LaraFoundry: No middleware mocking. Real HTTP requests. Assert what users experience. Banned user → can access support. PIN timeout → gets locked. Invalid session → gets logged out behavior tests > implementation tests #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
060
Dmitry Isaenko @dmitry-isaenko.bsky.social · 22/04/2026
LaraFoundry's filter pattern Request param `country=DE` → calls `country('DE')`. Method exists = filter applied. No config. No switch statements. whitelisted sort columns prevent SQL injection. word-by-word search for partial matching. #Laravel #PHP #LaraFoundry #Filters
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 16/04/2026
Testing Vue + Inertia in LaraFoundry: No component mounting. No jsdom. Just Pest feature tests asserting on Inertia props. If props are correct, the frontend works. If props are wrong, no frontend test saves you. #Laravel #Pest #Testing #LaraFoundry #BuildInPublic
021
Dmitry Isaenko @dmitry-isaenko.bsky.social · 15/04/2026
LaraFoundry's filter pattern - auto-discovery Request param `country=DE` → calls `country('DE')` method. No config. No routing. Method exists = filter applied. alphabetical nav, quick presets, sort with whitelisted columns - all the same pattern. #Laravel #LaraFoundry #PHP #Filters
110
Dmitry Isaenko @dmitry-isaenko.bsky.social · 15/04/2026
Pagination in LaraFoundry: Backend trait extracts metadata → controller passes it as prop → layout auto-renders paginator → component rebuilds URL keeping all filter params Pages don't know pagination exists. It just works. #Laravel #Vue #LaraFoundry #Pagination
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 15/04/2026
how child components trigger parent overlays in LaraFoundry: Layout: provide('showPulloutContragentFilter', showFilter) Page: const showFilter = inject('showPulloutContragentFilter') Pages don't manage z-indexes. Pages don't handle overlays. they just call a function. Layout does the rest. #Vue
000