Sign in

Dmitry Isaenko

@dmitry-isaenko.bsky.social
91 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
Krokq: krokq.com How it works (dev.tо): dev.to/d_isaenko_de... Public repo: github.com/dmitryisaenko/krokq_public ===== Other links: larafoundry.com - SaaS-core kohana.io - my first app (CRM/ERP, in dev) based on the larafoundry comentors.com - my second app (in dev too) based on the larafoundry
krokq.com
Krokq - task tracker & team chat in one place
A simple task tracker with built-in team chat: tasks, files, discussion and status together - not scattered across a dozen messengers.
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
Spot on! it’s all fun and games until a single missing nested key in a config array drops a silent 500 error. Strict DTOs and defensive validation at the core level are the only things keeping my sanity alive here.
010
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
thanks! honestly the trick was that both events existed before the feature. the core fires CompanyCreated for its own bookkeeping, billing fires the payment event anyway - affiliate just subscribes to them. the moment a growth feature needs the app to know about it, thats where the debt starts
010
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
dev.to/d_isaenko_de...
dev.to
Building a SaaS engine in public: an affiliate program that isn't one hardcoded scheme
For most of this series I have been shipping the parts of a SaaS that you can see: auth,...
000
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
Exactly!
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 10/06/2026
dev.to/d_isaenko_de...
dev.to
GDPR as a seam: when the right to access and the right to be forgotten are the same shape
I am building LaraFoundry, a reusable Laravel SaaS core, in public. It is extracted from a CRM that...
000
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
It’s actually already shipped and live! And we’ve moved way past that - v0.17.0 is already out now. Appreciate the support, building with that compound discipline is exactly the goal here!
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 09/06/2026
dev.to/d_isaenko_de...
dev.to
Letting admins edit email templates without handing them code execution
I am building LaraFoundry, a reusable Laravel SaaS core, in public. It is extracted from a CRM that...
000
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
dev.to/d_isaenko_de...
dev.to
I shipped a support desk by deleting a dependency
I added a support desk to LaraFoundry this week. The first commit in the slice removed a package...
000
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
dev.to/d_isaenko_de...
dev.to
"Notifications without WebSockets: an in-app centre and broadcasts on shared hosting
Every "add notifications to your Laravel app" tutorial seems to start the same way: install Pusher or...
000
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
Then it hit me. The seams I built to keep billing, navigation and dashboards decoupled in my Laravel SaaS engine were already freeze points. I had the team mechanism the whole time, solo. Full write-up: dev.to/d_isaenko_de... #larafoundry
dev.to
How a solo dev builds like a team: freeze the seams, not the plan
I build LaraFoundry alone. One person, one branch, one task at a time. A reusable SaaS engine for...
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
The real trick for parallel work is a smaller commitment, not a bigger doc. You freeze the contract between two modules. Just the interface they talk through. B mocks it, A implements it, nobody waits. Freeze means changing it is now an event with a protocol, not a quiet edit.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 08/06/2026
The map rots faster than you write it. Specifying module E, then building module A proves half of it wrong. And detail is not safety. A method signature is cheap and proves nothing. A vertical slice through tenancy, billing and auth is expensive and proves everything.
100
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
Full write-up, every hole and the fix: dev.to/d_isaenko_de...
dev.to
Extracting a QR login from a production app and closing 11 security holes before merge
QR login is one of those features that looks tiny on the roadmap and turns out to be a security...
000
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
dev.to/d_isaenko_de...
dev.to
Building a SaaS engine in public: a billing engine that isn't married to Stripe
A while back in this series I shipped the billing seam and was very careful to say it was not...
000
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
www.linkedin.com/posts/dmitry...
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
honesty section, every release has one: the console SHOWS subscription status (trial, active, expiring, expired) from the free core's columns. it can't manage subscriptions. that's the paid add-on. read-only on purpose. same free/paid line I drew with the billing seam.
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
the trap: a user can belong to several companies. suspend the active one and a naive check bounces every request they make, forever. so enforcement self-heals: promote the user's next un-blocked company, replay the request. none left? blocked screen. never a logout, never a loop.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
so company-block is a first-class thing now: one nullable column on the company, not mass-assignable. enforced in ONE place: the middleware every tenant-scoped request already passes through. one column, one boundary, takes the whole tenant down. nothing to forget on the next route.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
how the original CRM "blocked" a company: it didn't. you banned the owner, and a middleware denied anyone whose owner was banned. suspending a company meant punishing a person. two different operator actions, conflated into one. the audit trail told the wrong story.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
dev.to/d_isaenko_de...
dev.to
Building a SaaS engine in public: suspending a tenant without locking out the bystanders
I tagged v0.10.0 of LaraFoundry this week: the admin companies console. It is the second screen of...
100
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
honesty section, every release has one: this wires the access gate, but no caller consults it yet. enabling billing makes hasAccess() answer right; nothing in the core enforces it until that wiring lands. and there are zero real payments here. that's the paid add-on, later. seam only.
000
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
the gateway-agnostic shape is the point for non-US builders. most Laravel SaaS starters are Stripe-only. Stripe reaches ~46 countries. LaraFoundry's core names no provider. a host in a country Stripe doesn't reach implements the contract for its local PSP. same call sites.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
so the fake gateway didn't get extracted. the free core ships one driver: a null gateway that refuses every money operation loudly. no silent success. real drivers (Stripe, Paddle, a local PSP) come from the paid add-on via a Mail/Queue-style manager. swapping is one config value.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
what the original CRM did when a company "paid": $paymentStatus = 'success'; // hardcoded no Stripe. no gateway at all. "paying" wrote a row and moved a date. fine for a CRM with one user. poison for a reusable core: a success that's always true LOOKS like billing works.
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
dev.to/d_isaenko_de...
dev.to
Building a SaaS engine in public: shipping the billing seam, not billing
I tagged v0.9.0 of LaraFoundry this week: billing. Except the honest headline is that I shipped the...
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
github.com/dmitryisaenko
github.com
dmitryisaenko - Overview
Full-Stack Developer · Laravel · PHP · Vue. I build and run my own SaaS in public. Creator of LaraFoundry, an open-source SaaS core. - dmitryisaenko
000