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
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
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() 👇
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
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 👇
100
Dmitry Isaenko @dmitry-isaenko.bsky.social · 04/06/2026
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 👇
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
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 👇
300
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 · 03/06/2026
LaraFoundry: a modular Laravel SaaS core I'm extracting in public from a live CRM. Not a rewrite. Real production code, modernized, security-reviewed, shipped as a Composer package one module at a time. Auth, multi-tenancy, roles & permissions, with a write-up on each. #Laravel #BuildInPublic
050
Dmitry Isaenko @dmitry-isaenko.bsky.social · 02/06/2026
tagged v0.2.x of LaraFoundry: auth + users. decided early not to port my old hand-rolled auth. built on Laravel Fortify instead and only added what it doesn't cover: session tracking, OAuth, blocked-user gating. 114 tests. and 2 bugs they didn't catch 👇
110
Dmitry Isaenko @dmitry-isaenko.bsky.social · 02/06/2026
LaraFoundry: a modular Laravel SaaS core I'm extracting in public from a live CRM. Not a rewrite. Real production code, modernized, tested, shipped as a Composer package one piece at a time. Now: authentication on top of Fortify. Next: multi-tenancy. #Laravel #BuildInPublic #LaraFoundry
050
Dmitry Isaenko @dmitry-isaenko.bsky.social · 02/06/2026
v0.1.0 of LaraFoundry is the foundation layer: locale, query filters, middleware, an Inertia/Vue UI kit. 39 Pest tests, CI green on PHP 8.2/8.3/8.4. Auth, tenancy, billing = later phases. Undersell the tag, never the other way around. Built in public: github.com/dmitryisaenk... #Laravel #SaaS
github.com
GitHub - dmitryisaenko/larafoundry
Contribute to dmitryisaenko/larafoundry development by creating an account on GitHub.
030
Dmitry Isaenko @dmitry-isaenko.bsky.social · 02/06/2026
Shipping a Laravel core as a Composer package: the PHP was easy. The hard part was Vue inside vendor/ - import.meta.glob resolves relative to the host and breaks. Fix: stop letting the package own page resolution. Export modules, let the host wire routing. A library is not an application.
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 02/06/2026
A filter pattern from my production app, simplified: request key → method call on the filter object Elegant. Also: send ?apply=... and you're invoking arbitrary methods with input you control. Nobody exploited it in years. A Pest test closed it in an afternoon. #Laravel #PHP
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 02/06/2026
Extracting old production code into a reusable package isn't a refactor. It's a code review disguised as one. Tagged v0.1.0 of LaraFoundry today - the Laravel SaaS core I'm extracting from a live CRM. Moving the code into the light caught 2 real bugs that shipped to prod years ago.
110
Dmitry Isaenko @dmitry-isaenko.bsky.social · 29/05/2026
Payments module for LaraFoundry: - 2 tables: company_payments + promo_codes - multi-currency revenue stats with conversion - promo codes: percentage/fixed, per-user limits, personal codes - smart period filtering with COALESCE #Laravel #PHP #SaaS #BuildInPublic #LaraFoundry #OpenSource #Payments
280
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 · 29/05/2026
Payment events in LaraFoundry: CompanyPaymentProcessed → fires after payment NotifyOwnerAboutPaymentSuccess → queued job NotifyOwnerAboutPaymentFailed → queued job payment succeeds or fails, the company owner knows immediately. #Laravel #Events #Jobs #LaraFoundry
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 28/05/2026
Payments frontend in LaraFoundry: 5 Vue pages + 2 components. desktop table + mobile cards. payments and promo codes as tabbed interface. promo codes list shows: discount badge, usage counter, type badge, status badge, toggle button, payment count link. #LaraFoundry #Vue #InertiaJS #Frontend
040
Dmitry Isaenko @dmitry-isaenko.bsky.social · 28/05/2026
"Which payment is current?" in LaraFoundry: find MAX(paid_at) per company_id among successful payments. mark that row. admin sees exactly which subscriptions expire soon. no cron job. calculated on the fly. #LaraFoundry #Laravel #SaaS #Subscriptions
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 28/05/2026
Personal promo codes in LaraFoundry: set user_id on the promo code → only that user can use it. admin creates with user autocomplete (search by email/name, min 2 chars, max 10 results). great for VIP discounts. #Laravel #LaraFoundry #PromoCode #SaaS
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 27/05/2026
7 endpoints for promo code management in LaraFoundry: index + create + store + edit + update + toggle + search-users code and discount_type are immutable after creation. can't break existing payment references. #LaraFoundry #Laravel #CRUD #AdminPanel
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 27/05/2026
Date filtering in LaraFoundry payments: COALESCE(paid_at, created_at) successful payments have paid_at. pending/failed don't. COALESCE ensures every payment is filterable by date regardless of status. #Laravel #SQL #LaraFoundry #PHP
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 27/05/2026
Multi-currency totals in LaraFoundry: 1. Group payments by currency 2. Sum (amount - discount) 3. Convert to admin display currencies 4. Format with symbols configurable in config/own.php. your SaaS, your preferred currencies. #Laravel #LaraFoundry #MultiCurrency #PHP
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 26/05/2026
Admin payment dashboard in LaraFoundry: Revenue totals per currency, auto-converted to display currencies. $12,500 / €11,200 / ₴35,000. filter by period, status, plan, country, promo code, email, company name. totals update with every filter change. #LaraFoundry #SaaS #AdminPanel #Laravel
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 26/05/2026
Promo code has 4 statuses in LaraFoundry: active - is_active AND not expired AND not exhausted inactive - manually deactivated expired - past expires_at exhausted - used_count >= max_uses one getStatus() method. used in filters and badges. #Laravel #LaraFoundry #PromoCode #Architecture
020
Dmitry Isaenko @dmitry-isaenko.bsky.social · 26/05/2026
Failed payment with a promo code in LaraFoundry? The code isn't consumed. only successful payments count against the single-use-per-user limit. subtle but important. #LaraFoundry #Laravel #Payments #PHP
010
Dmitry Isaenko @dmitry-isaenko.bsky.social · 25/05/2026
Promo code system in LaraFoundry: - percentage or fixed discount - global max uses - single use per user - personal codes (tied to specific user) - expiration dates 4 constraints, all checked in one method. code stays clean. #Laravel #LaraFoundry #PromoCode #SaaS
010