Skip to content

Unconfirmed, not yet published. Full legal name as it should appear on a contract or invoice Not supplied. A portfolio is not the place to guess a legal name, and the salutation is the one string a recruiter reads first. — portfolio

Systems that hold up.

I build the software layer that runs inside ERP and operational systems, and the AI and automation that sits on top of them.

role
Independent AI-platform and ERP builder
based in
Unconfirmed, not yet published. City and country of residence Stated as Bulgaria in the brief; the city was not supplied and is not inferable. Timezone matters to an ESN buyer, so this is worth pinning.
availability
Unconfirmed, not yet published. Current availability status and start date An availability claim is a commitment. Guessing one is worse than declining to make it, so it stays open until answered.

For an ESN or contract buyer

Contract availability, breadth across stacks, and evidence of shipping under someone else’s constraints.

For an ERP or platform role

Data models, tenancy, workflow engines, deployment topology, and the decisions behind them.

01A live system, and the decisions in it

One textile manufacturer, five components, and the reasoning between them.

This is a system that is running now. Select any component for what it does, what it is built with, and the decision that shaped it. Every decision here includes what it cost, because a decision with no downside is a slogan rather than an engineering choice.

deployment topology

text version
  • ERP coreapplication

    The system of record for the business: orders, stock, production and the financial perimeter. Everything else either feeds it or reads from it.

  • Primary databasedata

    Holds the operational record. Separate lifecycle from the application so the two can be failed, restored and scaled independently.

  • Configurator addonextension

    A print-on-demand configurator: a catalogue of artwork with its own hierarchy, tagging, ordering weight, per-artwork default colour and scaling limits, so product images can be recoloured and scaled per order instead of being one flat file per variant.

  • Edge proxyedge

    Terminates TLS and routes by hostname. The only component with a public interface, and therefore the only one that is an attack surface worth hardening.

  • Public storefrontstorefront

    The public face: the site a customer actually visits. Runs on its own infrastructure with its own lifecycle, separate from the operations system.

ERP core

application

The system of record for the business: orders, stock, production and the financial perimeter. Everything else either feeds it or reads from it.

technology
Odoo 18, Python
exposure
Internal production system. Not published to the internet.

operational decision

Never modify the framework. All behaviour ships as separate addons.

why
The framework is the part that has to survive an upgrade. Anything written into it becomes a merge conflict and a delay every time the vendor ships a release, and it is the change most likely to be lost in a handover.
what it cost
I have to live inside the extension model, including its rough edges. Some things that would be five lines of core patching are a workaround in an addon. I would rather pay that cost than own an un-upgradeable system.
ask me this
Ask what the extension model forced me to do badly, and how I worked around it.

operational decision

The ERP is bound to the loopback interface only. No public address.

why
It is a production operations system holding commercial data. It has no reason to be reachable from outside, so the cheapest and strongest control available is to not give it an interface to be reached on. Whatever the public needs is served by a separate front.
what it cost
No direct external access for anything that would be convenient to expose, including some internal mobile access. Every such request has to be solved through a gateway instead.
ask me this
Ask what I would need to expose, and what I would put in front of it.

flows involving this component

  • order intake: storefront → erp

    A purchase becomes an order in the system of record through an authenticated interface. Deliberately not a shared database or a shared filesystem: a queue that can be inspected and retried beats a coupling that cannot.

  • operational writes: erp → data

    All reads and writes go through one database service. No component holds its own copy of operational state, because a second copy is a second source of truth and reconciliation work that nobody schedules.

  • model extension: addon → erp

    The configurator adds artwork and category models and hooks them into the product and order flow. It reads and writes through the framework, so the framework stays responsible for access control and transaction boundaries.

  • internal access: edge → erp

    Reached only from inside the deployment. There is no route to the ERP that starts on the internet.

  • Containerised, one application per compose file, so each can be rebuilt and rolled back on its own.
  • Runtime secrets are held in platform-level secret storage rather than in images, compose files or version control.
  • The ERP platform is never edited in place. Every change is a versioned module that can be traced and reverted.
  • Public and internal surfaces are separate by construction, not by firewall rule.

Published deliberately as components and reasoning. Hostnames, addresses, ports, credentials and schema internals are withheld: they are the implementation, and this is a portfolio.

02ERP and business systems

Four verticals, and the model underneath each one.

ERP work is not configuration. The recurring problems are tenancy, workflow state, and integration, and each vertical below solved them differently because the constraint was different.

Textile manufacturing

Unconfirmed, not yet published. Client name and whether this may be published The engagement is real and the system is live, but the client has not signed off on being named on a portfolio. Naming a manufacturer is a commercial disclosure, so it waits for permission.

Made-to-order production where a single order becomes many operations, and the artwork applied to each piece is a variable rather than a fixed attribute. Stock counts alone do not describe what the floor is actually doing.

isolation by deployment boundary

isolation by deployment boundary. One deployment per tenant. No shared schema, so there is no row-level filter that a single forgotten clause could defeat.

tenancy
Single-tenant per deployment, which is the honest answer for a manufacturer of this size. Multi-tenancy would have bought isolation the business does not need at the cost of a shared schema and a filtering discipline that is easy to get subtly wrong. Isolation is enforced by deployment boundary instead of by a row-level rule that could be bypassed by one forgotten filter.
workflow
Staged task lifecycle rather than a single status field: an order moves through production stages, and each stage owns its own completion rule and its own downstream actions. A status string cannot express "cut but not yet pressed", and trying to encode it in one field is how a shop floor ends up with a state machine nobody documented.
integration
External systems connect through connection records that hold runtime credentials, so a credential is issued per environment and lives in the platform rather than in a compose file. Revoking access is a matter of rotating one record.
each stage owns its own completion rule

Ordered stages rather than one status field. Each stage owns its completion rule and its downstream actions. A single status field cannot express "cut but not yet pressed", which is how a shop floor ends up with a state machine nobody documented.

the decision

Model production as ordered stages, not as one status on the order.

The floor needs to know what is next and who owns it. A single status answers "where is this" but not "what happens next", and the second question is the one the shop actually runs on.

cost:More moving parts than one field, and every new stage is a schema change rather than a configuration tweak. In exchange the board view is derived from the data instead of maintained beside it.

ask:Ask what happens to work already in progress when a stage is reordered.

Call centre operations

Unconfirmed, not yet published. Client name and publication permission Undisclosed. The vertical is described because the engineering is the point, not the client list.

High-volume, short-interval work where the interesting constraint is not data volume but state volume: campaigns, queues, dispositions and outcomes all change at once, and a call centre lives or dies on whether the record of a call is correct the moment it is logged.

shared platform, scoped at the access layer

shared platform, scoped at the access layer. Several tenants share one database. Isolation is applied where access is granted, so a query with no tenant scope returns nothing rather than another tenant's rows.

tenancy
Multi-tenant with per-tenant isolation, which this vertical genuinely needs: several client brands share one platform but must not see each other's contacts, campaigns or reporting. Isolation is applied at the access layer so that a query without a tenant scope cannot return another tenant's rows, rather than relying on every caller remembering to add a filter.
workflow
Contact and campaign records carry their own lifecycle so that a conversion can be audited after the fact. Disposition, opt-in state and consent are stored as dated records rather than a current-value flag, because a compliance question is about what was true at a moment in time, not what is true now.
integration
Outbound and telephony integrations behind connection records, so a provider credential can be rotated without a deploy and a misconfigured provider fails in one tenant's queue rather than globally.

the decision

Consent and disposition are append-only dated records, not current-value fields.

The question "was this contact contactable on the date we called" is historical and has to be answerable exactly. A mutable flag answers a different question and cannot be reconstructed.

cost:More rows and a read model for current state. Every current-state query needs the latest entry, which is a join or a maintained projection.

ask:Ask how the current state is derived without letting the derivation become a second source of truth.

E-commerce

Unconfirmed, not yet published. Client name and publication permission Undisclosed.

Catalogue, stock and order flow under continuous change, with the public storefront and the operational system managed by different hands and on different cycles.

shared platform, scoped at the access layer

shared platform, scoped at the access layer. Several tenants share one database. Isolation is applied where access is granted, so a query with no tenant scope returns nothing rather than another tenant's rows.

tenancy
Shared platform, isolated records. The public side is the exposed surface, so its reads are cacheable and its writes are authenticated and replay-safe: a retried order submission must not create a second order.
workflow
Order lifecycle is the spine, with fulfilment and stock reservation bound to state transitions rather than to code paths, so the same transition cannot be bypassed by calling a different method.
integration
Storefront to ERP through an authenticated, retryable interface. Orders are idempotent on a client-supplied key, which is what makes a retry safe instead of a duplicate.

the decision

Order intake is idempotent on a client-supplied key.

Any network call can be retried, and a retried create that is not idempotent is a duplicated order the customer sees and staff have to reconcile. This is the single cheapest guard against the most common class of commerce bug.

cost:Keys have to be generated and stored by the caller, and retained indefinitely for the guarantee to hold. Every write path has to remember to honour it.

ask:Ask what the key is, and how long you keep it.

Property

Unconfirmed, not yet published. Client name and publication permission Undisclosed. The brief notes the public site is WordPress-based; whether custom development was in scope is separately unconfirmed and is not claimed here.

A catalogue of genuinely distinct assets, each with its own availability calendar, enquiry handling and lifecycle. The difficulty is not listings, it is that availability is authoritative and contended.

authoritative state, cached reads

authoritative state, cached reads. Contended state stays in exactly one place. Reads that can be static are cached; the value everyone must agree on is never copied.

tenancy
Read-heavy public surface with a small, well-defined write surface for agents. Listings are cached aggressively because they are read far more than they change, and a stale listing is a far smaller problem than an unavailable site.
workflow
Enquiry and viewing lifecycle, kept distinct from the listing lifecycle. A property does not become unavailable because an enquiry arrived, and conflating the two states produces a listing that quietly disappears.
integration
Enquiries flow into the operational system as records rather than as email. Email as an integration bus is unauditable: the record of what was promised to a prospect should not depend on an inbox.

the decision

Availability lives in exactly one place and the public listing reads it, rather than caching availability into the listing record.

Availability is contended: two agents can act on the same property. If a copy is cached into the listing, the conflict resolves by whoever wrote last and the loser finds out from a disappointed client.

cost:A read that could be static now depends on live state, so the listing page needs a cache whose lifetime is shorter than the availability lifecycle.

ask:Ask how you would detect two agents booking the same property.

03Web and commerce

The public edge, operated rather than written.

Deployment, configuration, hardening and operations. I am not claiming PHP or plugin development depth I cannot demonstrate, and the scope below is stated accordingly.

Textile client storefront

live in production

WordPress, Astra, Elementor, WooCommerce, Yoast, GA4 via Site Kit

Deployed and operated. Public-facing commerce for the textile vertical.

The public side of the textile vertical: catalogue, made-to-order variation, SEO, and analytics. This is the public edge of the system shown above, running on separate infrastructure from the operations system behind it.

Unconfirmed, not yet published. Permission to publish the storefront URL The site is live and verified, but a client URL on a portfolio is a commercial disclosure and needs sign-off. Publishing a working link to someone else's business without asking is not a judgement call I get to make quietly.

the decision

Analytics runs through a first-party Site Kit integration rather than a hard-coded tag.

A hard-coded measurement tag in a theme is a permanent dependency on someone else's script in the critical path of every page, and it cannot be turned off without a deploy. Configuring it through the platform keeps the decision reversible from an admin screen.

cost:One more moving part between a visit and a recorded visit, so analytics can silently under-report and nobody notices for weeks.

04AI orchestration

The architecture, not the vocabulary.

A list of model names tells a platform reviewer nothing. What is worth discussing is where the trust boundary sits, how retrieval is shaped, and what happens when the agent is wrong.

the trust boundary

Every layer below shares one boundary: an agent that can read is an agent you can audit. An agent that can write is an agent that can be talked into writing, so the two are separated and escalation is explicit.

read by default · write by opt-in

Read and write tools are separated, and write tools require explicit opt-in. The default posture is inspection; escalation is a deliberate act. This reduces blast radius rather than removing the risk.

Retrieval

Embeddings and a retrieval layer, with the corpus shaped before the model.

Retrieval quality is decided by how the corpus is chunked, labelled and routed, not by the embedding model. Most of the work is in deciding what counts as one retrievable unit and what belongs beside it, because a chunk that splits a decision from its rationale retrieves as two useless halves.

Route by domain before retrieving, rather than retrieving across one large index and hoping the ranking is precise enough.

A single index degrades as it grows: the same query returns neighbouring-but-irrelevant neighbours, and precision falls without any component reporting that it had fallen. Routing keeps each retrieval inside a coherent space and makes the result explainable.

cost:Every new domain needs a route, and a query that spans two routes needs a composition step. Also a wrong route is worse than no route, because the wrong space looks confident.

Agent lifecycle

Provisioning and lifecycle management, not a single long-running agent.

Agents are provisioned, versioned, given scoped credentials, observed, and torn down. Treating an agent as a process to start rather than a capability to define is what makes the difference between a demo and something you can run unattended: a lifecycle implies a teardown, and a teardown implies you know what it was holding.

Each agent gets credentials scoped to the tools it needs, issued at provision time and revoked at teardown.

A shared credential set means any agent can do anything any other agent can do, so one compromised prompt or one buggy tool call has the blast radius of the whole platform. Scope also makes the audit question answerable: which agent touched this record.

cost:Credential plumbing for every agent, and a real cost the first time an agent needs a new capability. It also means an agent that needs two things has to be told which is which.

Integration surface

Tool definitions exposed over a protocol, so tools are described once.

Tools are declared rather than hand-wired into a prompt, so the schema is the contract. This means a tool can be added without editing orchestration code, and it means an invalid argument is rejected at the boundary with a useful message instead of failing somewhere downstream as a null.

Read and write tools are separated, and write tools require an explicit opt-in rather than being available by default.

An agent that can read is an agent you can audit; an agent that can write is an agent that can be talked into writing. Separating them means the default posture is inspection, and escalation is a deliberate act.

cost:A task that needs both cannot be done in one pass, and the escalation is a context switch. Also a determined attacker with write access still has it, so this reduces blast radius rather than removing the risk.

Serving

Local model serving, so inference is not a per-request third-party dependency.

Serving locally turns inference from an external call with someone else's availability, pricing and data handling into a process with a queue. That matters most for the workloads that are large and dull: classification, routing, extraction, where latency and volume are predictable and a per-token cost is a rounding error next to the engineering.

Split by task: local for volume and classification, hosted for the reasoning that actually needs a frontier model.

Sending every classification to the strongest available model is the expensive way to do the easiest work. The split is drawn where the quality difference stops mattering and the volume difference starts.

cost:Two serving paths to operate and to keep consistent, and a routing decision that has to be made correctly per task or quality quietly drops.

05Infrastructure and operations

The unglamorous half, which is most of the job.

None of this is novel and all of it is the work. The recurring theme is making the failure path boring: a rollback that is a diff, a restore that is one named thing, a public surface small enough to read.

one file per application

Each application is a separate compose file, so a rollback is a diff against one named application. Services bind to loopback; the proxy is the only public interface.

the shape of the deployment

Three properties decide how this fails. The unit of rollback is the unit of deployment. The public surface is one proxy, so it is the one place worth auditing. And a service that is not published cannot be connected to, which is a control that cannot be misconfigured.

One application per compose file

Container topology, split so each service can fail and roll back alone.

Each application gets its own compose file and its own service boundary. This is not tidiness: it is what makes a rollback a single file change against one named application, instead of a coordinated edit to a file that also describes six other things.

Split per application rather than one large compose file per host.

A rollback needs to be boring. Boring means the unit of rollback is the unit of deployment, so reverting is a diff against one file rather than a reasoning exercise about which services changed together.

cost:Shared infrastructure such as the proxy and the network has to be referenced from several files, so there is a consistency risk that a single file would not have.

ask:Ask how a shared network change is rolled out without stopping everything.

Edge and TLS

Automated certificates, and a public surface small enough to audit.

Caddy in front, handling certificates and hostname routing. The design goal is a public surface small enough to read in one sitting, because every externally reachable service is something to patch forever.

Only the public front is exposed. Internal services bind privately.

Every published port is a maintenance obligation and a scanning target. Not publishing a port is a control that cannot be misconfigured, because there is nothing to connect to.

cost:Any internal-only service needs a deliberate, authenticated path out, which is more work than opening a port and slightly less convenient.

ask:Ask what you would need to expose to debug something at 2am.

Service lifecycle

systemd units, process supervision, and logs that answer questions.

Long-running processes supervised rather than left to whatever shell started them, with logs written somewhere queryable. The goal is that restarting a service and finding out why it died are both routine and documented operations rather than improvisation.

Structured logs retained outside the container lifecycle.

A container that logs to stdout and dies takes its diagnostics with it unless something outside is collecting. The failure you most need to debug is the one that just happened.

cost:A log pipeline to maintain and a retention decision with a storage cost, and log volume has to be bounded deliberately or it becomes the outage.

ask:Ask how you would find the one request that failed this morning.

Security hardening

Boring controls, applied consistently, over clever ones applied once.

Least privilege on processes, secrets out of images and version control, least-privilege database roles, and internal services with no user enumeration or management endpoints reachable from outside. None of this is novel and all of it is the work.

Secrets live in platform-level secret storage, referenced at runtime.

A secret in an image is in every layer of that image forever and in every registry it has ever been pushed to. Rotating it then means rebuilding and redistributing, which nobody does under pressure.

cost:Runtime secret references are a moving part, and a misconfigured reference fails at start rather than at build, which is a worse time to find out.

ask:Ask what you would do about a secret that was already committed once.

Progressive web frontends

Installable frontends for operational use on imperfect connections.

PWA frontends for staff-facing operational views, where the requirement is installability and offline-tolerant reads rather than a store presence.

Cache for reads only, and never present stale data as current.

An offline cache that silently shows yesterday's stock is worse than an error, because it is trusted. Reads can be cached if the interface is honest about when the data is from.

cost:Honest staleness needs a visible timestamp and a clear invalidation path, which is more interface work than caching silently.

ask:Ask what the user sees if they act on data that is a minute old.

06Exploring, clearly labelled

Interests, not projects.

None of these has a client artifact, so none of them appears as delivered work. They are here because the direction is real, and pretending otherwise in either direction would be dishonest.

  • Robotics and control

    direction of interest, no client artifact

    Inverse kinematics, motion planning, and the tolerances between a solved solution and a physical one. Built a two-link solver to understand what is actually hard about it: unreachable targets, not the trigonometry.

  • Satellite and orbital data

    direction of interest, no client artifact

    Working with real orbital telemetry and deriving ground tracks and orbital periods rather than drawing plausible curves. The interesting part is that the answer has to be right, which makes it a good test of whether a visualisation is honest.

  • Gear reducers and drivetrains

    self-study

    Modelling ratios, backlash and efficiency so a design choice has a number attached instead of an opinion.

  • Drones

    self-study

    Autonomy stacks and the failure modes that come with them. Currently reading, not building.

  • 3D and fashion

    direction of interest, no client artifact

    Garment simulation and fit. Interested in it because it sits between manufacturing and software, which is where this page has been going.

07Honest gaps

What I have not done, and what would close it.

Listed because a reviewer who finds an unclaimed weakness stops trusting the claims. Every item here has a closing plan, which is the difference between self-awareness and apology.

Financial and accounting process ownership

I can configure and integrate against the financial perimeter of an ERP, and I have worked in systems where it mattered. I have not owned a month-end close, a reconciliation policy or a statutory reporting calendar, and I would not claim to.

closing:This is learnable in weeks on a real system and is exactly what a finance domain owner would be asked to teach on day one.

Client-facing delivery and PM coordination

Most of my recent work has been technical delivery into an existing engineering process, often alongside other engineers. I have run discovery and stakeholder conversations, but I have not owned a commercial relationship, a scope negotiation or a client escalation.

closing:I would rather be honest that I have not run a commercial relationship than imply it. It is a different skill from the engineering and I would want support doing it.

Enterprise-vendor ERP ramp-up

Odoo I know from shipping. SAP, Dynamics and SOFTONE I have read about and had exposure to, and I have no production depth in them. I would not pretend a week of reading equals delivery experience.

closing:The transferable parts are real: data modelling, staged workflow, tenancy, integration. The vendor-specific parts are the ramp, and they are the smaller half of the job.

WordPress development depth

I have deployed, configured, hardened and operated WordPress sites, including a production storefront. I am not claiming PHP or plugin development work at a level I cannot show, and the storefront on this page is presented as deployment and operations work.

closing:I can show the operations side properly, which is the part that decides whether a site stays up.

08Contact and availability

Get in touch.

Contact details are still being confirmed and are marked rather than guessed. If you have a role that fits the intersection above, the fastest route is LinkedIn.

EmailUnconfirmed, not yet published. Primary email address Deliberately not published from a build environment. This is the single most important field on the page and it has to be a real, monitored inbox.
PhoneUnconfirmed, not yet published. Phone number, ideally with country code EU recruiters tend to call rather than mail on contract roles.
LinkedInUnconfirmed, not yet published. LinkedIn profile URL Not supplied.
GitHubUnconfirmed, not yet published. GitHub profile URL Not supplied.

about the markers on this page

Every fact on this page is either verified and shown as a value, or unverified and shown as a hatched marker. Nothing is filled in with a plausible-looking guess. That is a deliberate design decision rather than an unfinished page: a portfolio that quietly invents a phone number is worse than one that admits it is missing one.