< ENGINEER / >
CraftCode
< ENGINEER / >
About Me
N°02 / 07
I build production web and mobile products end to end: from architecture and APIs to polished interfaces, testing, and deployment.
Shipped a psychometric SaaS platform to production in five months as the sole developer.
Built and shipped a cross-platform Flutter app from a single codebase to both the App Store and Google Play.
Core Stack
Also expert in GraphQL, Redis, WebSockets, and automated testing.
III
Experience
N°03 / 07
Three years across product startups and client work, moving from a single React site to owning a platform end to end.
Jan – Jul 2026 · 7 mos
Amman, Jordan · Remote
The Little Genius
Shipped Inside Brain, a production psychometric assessment platform, solo in five months: engineered the TypeScript/GraphQL backend, built the Next.js product across six timed assessments, and shipped a Dockerized deployment scoring Lighthouse 100 for accessibility and SEO.
Jul 2024 – Feb 2026 · 1 yr 8 mos
Dubai, UAE · Remote
Masaar Media
Ran technical SEO and performance audits across client sites, contributing to 30–40% page-speed and search gains, delivered responsive builds for 4+ clients, and built 3 GA4/GTM/Looker Studio dashboards.
Aug 2022 – Oct 2023 · 1 yr 3 mos
Bethlehem, Palestine · Hybrid
Students’ Forum Association
Built the association’s first proper web platform end to end, a React front end on an Express API, replacing an unmanaged patchwork of data.
IV
Projects
N°04 / 07
A selection of shipped products spanning full-stack web apps, mobile, and backend tooling.
WordPress
POS marketing site
Flutter · Firebase
Tutor matching app
Flutter · Suoabase
On-demand cleaning
Node.js · Go
POS data sync, zero loss
Flutter · AI
Live sermon translation
NATS · Kubernetes
Event-driven system
Flutter · Firebase
Gym management app
GraphQL · Neo4j
IEEE-published capstone
V
Engineering Decisions
N°05 / 07
The reasoning behind consequential technical choices, including the constraints, trade-offs, and measured result.
01–02 / 06
Platform strategy · Performance
A React product with performance, responsive UX, and SEO issues was headed toward a full WordPress rebuild of more than 40 existing pages.
Decision Diagnose and improve the existing implementation before replacing the platform.
An existing React-based product had accumulated several visible problems:
A proposal was made to rebuild the website on WordPress. The main motivation was to improve content management, SEO, and the overall website experience.
The person advocating for the migration was already familiar with WordPress, which made it a natural solution from their perspective.
The proposed direction was effectively:
Existing React application → Full WordPress rebuild
I disagreed with the migration during three separate discussions.
My concern was not that WordPress was inherently a bad technology.
My concern was that we were about to replace an entire working system before proving that the platform itself was the cause of the problems we were trying to solve.
The actual problems were performance, UX/UI, responsiveness, and SEO.
A full rewrite introduced a much larger problem: rebuilding more than 40 existing pages and recreating a complex interactive experience simply to address issues that might be solvable inside the current system.
I separated the problem from the proposed technology.
Should we use React or WordPress?
I focused on a more useful question:
What exactly is broken, and does fixing it require replacing the platform?
A complete migration would have meant:
Do not rewrite the system yet. Diagnose and fix the actual problems first.
Instead of replacing the application, I proposed improving the existing implementation directly.
Diagnose → Optimize → Measure → Re-evaluate
Images were resized appropriately instead of serving unnecessarily large assets. Formats were improved, including using WebP where appropriate. Lazy loading kept images outside the initial viewport from loading immediately.
I reduced situations where the initial user experience unnecessarily waited for external data before allowing the page to become usable. External dependencies became less capable of blocking perceived loading.
The existing application was improved instead of assuming SEO required a new platform. This included adding and correcting technical discovery mechanisms such as robots.txt and the sitemap.
The existing UI was improved to behave correctly across devices and screen sizes instead of treating the mobile experience as an afterthought.
After further research and discussion, the team abandoned the proposed WordPress migration. We kept the existing application and improved it instead.
The measured performance score improved from 55 to 92 on desktop and from 38 to 84 on mobile.
The site also became faster to load and more usable across different screen sizes. Instead of spending months recreating more than 40 pages, the team retained the existing system and addressed the problems directly.
The important part of this decision was not choosing React over WordPress. It was refusing to confuse a problem in an implementation with a problem in the underlying platform.
A rewrite can feel attractive because it creates the impression of starting clean. But a rewrite is justified only when the expected value of replacing the system outweighs the cost, risk, and effort of rebuilding what already works.
In this case, the problems could be substantially improved without replacing the product.
Do not replace a system merely because it has problems. First determine whether those problems actually originate from the system's architecture or from fixable implementation decisions.
Or more simply:
Fix the constraint, not the technology you happen to dislike.
The goal is not to be the engineer who always argues against rewrites. The goal is to be the engineer who can answer:
What problem are we actually solving, why is this solution appropriate, what will it cost us, and how will we know whether the decision worked?
AI architecture · Integration boundaries
A LangGraph workflow needed a company API, but direct integration would let provider schemas, authentication, errors, and business rules leak into the core.
Decision Put an internal capability contract and provider adapters between the AI workflow and external APIs.
I was working on a shared AI project built with LangGraph. The system needed to interact with an external company's API as part of the AI workflow.
During the planning phase, the most direct implementation would have been:
LangGraph workflow → Company API
That approach would have been simple initially, but it created an architectural risk. The product was not expected to remain tied to a single company forever. Other companies could eventually connect their own systems, APIs, schemas, authentication mechanisms, and business rules.
The problem was therefore not:
How do we connect LangGraph to this API?
The more important question was:
What happens to the core AI workflow when the next company's API looks completely different?
If the LangGraph workflow depended directly on the first company's API, details specific to that provider could gradually leak into the core application.
LangGraph Workflow
|
+ Company A request and response mapping
+ Company A status and error handling
+ Company A assumptions
That would work for the first integration. But when another company was introduced, the system could start accumulating provider-specific branches:
if provider == CompanyA: ...
elif provider == CompanyB: ...
elif provider == CompanyC: ...
At that point, adding integrations would no longer be an integration-layer concern. It would require repeatedly modifying the AI workflow itself.
I wanted the core LangGraph workflow to understand the business capability it needed, not the implementation details of the company providing that capability.
Instead of asking:
Which endpoint should this graph node call?
I reframed the problem as:
What contract does the AI workflow actually require from an external system?
That distinction led to the architectural decision.
I redesigned the integration around an adapter layer. Instead of allowing LangGraph to communicate directly with a specific provider API, the workflow communicates through an internal contract.
LangGraph AI Workflow
|
| Internal Contract
v
Integration Layer
|
+-----+-----+
| | |
Adapter A B C
| | |
Company A B C APIs
Each adapter is responsible for translating between the application's internal model and the external provider's model.
Provider-specific concerns stay behind the adapter boundary.
External API response
v
Provider Adapter
v
Internal normalized representation
v
LangGraph workflow
LangGraph action
v
Internal integration contract
v
Provider Adapter
v
Provider-specific API request
This allows the core AI workflow to remain focused on reasoning, state transitions, orchestration, decision logic, and business behavior rather than external API details.
The goal was not to introduce a design pattern simply because an adapter is considered a good practice. The abstraction addressed a specific expected source of change.
The external provider was likely to change. The core business workflow should not need to.
Put the abstraction at the boundary where variation is expected.
Instead of trying to make every part of the system generic, I isolated the part most likely to differ between companies.
The simplest initial implementation was LangGraph calling the company API directly. It offered faster initial delivery, fewer abstractions, and less code at the beginning.
The problem was that the core workflow would become aware of the first provider's API model. Adding another provider could then require changes throughout the graph. Short-term simplicity could become long-term coupling.
Provider conditions could live directly inside LangGraph. This could work with very few integrations, but it makes orchestration responsible for AI reasoning, business logic, API mapping, provider selection, error translation, and integration behavior.
The workflow would become increasingly difficult to reason about.
CORE
LangGraph, AI behavior, business workflow, internal domain model
INTEGRATION BOUNDARY
Internal interface / contract
EXTERNAL
Adapter A -> Company A
Adapter B -> Company B
Adapter C -> Company C
The core depends on an internal capability, not on a specific provider.
New provider
v
Modify LangGraph
v
Add conditions and mappings
v
Modify error handling
v
Retest core workflowNew provider
v
Implement new adapter
v
Map behavior to internal contract
v
Reuse existing core workflowThe core does not need to know whether the external implementation is Company A, Company B, Company C, or a future system that does not yet exist.
The AI workflow would not be designed around one company's API.
LangGraph nodes would remain focused on orchestration rather than API translation.
Supporting additional companies would not require spreading provider checks across the workflow.
External field names and structures would not become the application's internal domain model.
The boundary was introduced while the architecture was still being designed, before multiple providers became tightly coupled to the system.
The adapter architecture was not free. It introduced an additional abstraction, more interfaces, mapping logic, additional test boundaries, and slightly more initial implementation work.
For a product permanently connected to only one external API, this abstraction could have been unnecessary. It was justified here because supporting systems from other companies was a realistic architectural requirement.
The additional complexity was placed specifically where future variability was expected.
The separation creates clearer testing boundaries. The core workflow can be tested against the internal contract with a fake adapter, without requiring the real provider API.
Provider adapters can be tested independently for request mapping, response normalization, authentication, API errors, malformed responses, timeout behavior, and provider-specific edge cases.
This keeps failures in external integrations from unnecessarily contaminating tests of the AI reasoning workflow.
This project involved a real company integration. For confidentiality reasons, the company, business domain, API details, data structures, and internal project information are intentionally omitted.
The architectural decision can still be explained without exposing proprietary information. The important engineering problem was not the identity of the provider. It was the system boundary.
Do not let an external system define the architecture of your core application.
External APIs, partners, schemas, authentication, and business integrations change. The core system should absorb as little of that volatility as possible.
Abstract around expected change, not around hypothetical complexity.
I did not attempt to make the entire system generic. I isolated one boundary because there was a concrete reason to expect multiple implementations behind it.
Adding another provider should require another adapter, not another version of the AI workflow.
That became the test for whether the boundary was designed correctly.
The important decision was not "Use the Adapter Pattern." It was:
Recognize which part of the system was likely to change, and prevent that change from spreading into the core architecture.
Integration reliability · Failure recovery
A scheduled invoice and POS sync could complete most records successfully while a small subset failed because of validation, remote API, or data-mapping errors.
Decision Preserve successful work, isolate failed records, and retry only the unresolved subset.
Kamakan Middleware moves sales invoices, products, and inventory between a POS platform and an internal backend. Some operations run on a schedule. Others start from the dashboard or from changes arriving in the application.
The awkward part was not sending a request. It was deciding what a failure meant when a batch contained hundreds or thousands of independent records.
A bulk request could create most invoices and reject a few. A POS push could update one product, fail on the next, and continue successfully after that. Treating those outcomes as a single success or failure would discard useful information.
The simplest recovery plan was to fail the whole job and replay the complete batch. That sounds safe until successful records already exist.
The system needed to distinguish a transport failure, where the operation could not complete at all, from a partial data failure, where only specific records remained unresolved.
Preserve confirmed progress and make the failed subset the unit of recovery.
The backend reports created, skipped, and failed invoice records separately. Duplicate invoices are skipped rather than turning a safe replay into another failure. The API accepts batches of up to 2,000 invoices, but it does not pretend that every item in a valid request has the same outcome.
The Go scheduler reads the item-level errors, extracts only the failed invoices, and retries that smaller set. It carries their original indices forward so a later error still points to the correct record in the source batch.
POS fetch or backend call fails
v
Immediate retry tier
v
Delayed retry tier
v
Final failure alert
Bulk request completes
v
Extract failed indices
v
Retry failed subset
v
Log unresolved records
Immediate retries cover short interruptions. Delayed retries cover outages that need time to clear. Partial retries do neither blindly; they operate on the records the backend explicitly rejected.
Product and inventory pushes use the same principle with a different mechanism. Push jobs run one at a time through a queue, preventing overlapping jobs from writing competing state. Each run records successful and failed items separately and publishes a partial status when both are present.
Failed product IDs and inventory adjustment numbers are merged into a Redis-backed cache. A successful retry removes the corresponding entry. Retry counts stop permanent failures from cycling forever, and the cache evicts its oldest entries when it exceeds the configured limit.
An in-memory implementation follows the same contract when Redis is not configured, which keeps unit tests isolated without changing the recovery behavior being tested.
Recovery is only useful if the result is visible. Kamakan records success, partial success, and final failure as different states. Logs retain counts and failed-item details. WebSocket events update the dashboard, while email alerts identify invoice records that remain unresolved after the configured retry rounds.
This gives an operator a list of remaining work instead of a generic message that the nightly sync failed.
Item-level recovery adds state. Error indices must be remapped. Failed records need bounded storage. Retry counts and race conditions require tests. A single all-or-nothing status would be easier to implement.
The extra machinery is justified because these records are independent and the external systems can fail independently. Preserving a completed record is safer than repeatedly treating it as unfinished.
Recovery scope should match failure scope.
When one request fails, retry the request. When three records fail inside a successful batch, retry those three records. Broad recovery is not automatically safer; sometimes it only repeats work the system has already confirmed.
Service boundaries · Operational control
Long-running schedules, delayed retries, external POS calls, API traffic, persistence, and live dashboard updates had different runtime and failure characteristics.
Decision Keep orchestration in a Go scheduler and application responsibilities in the Node.js API, connected through explicit HTTP and Redis contracts.
Kamakan needed an application API and an integration runtime. Those responsibilities touched the same business data, but they did not behave like the same process.
The API serves authenticated requests, validates payloads, writes to SQL, exposes dashboard data, and publishes live events. The integration runtime wakes on a schedule, calls an external POS, waits between retry rounds, transforms records, and can remain busy long after an HTTP request should have ended.
Putting cron execution inside the Node.js API would have reduced the number of services, but it would also have joined unrelated lifecycles.
The language was not the important decision. The lifecycle was.
GO SCHEDULER
Cron and timezone
POS extraction
Retry timing
Transformation
Operational alerts
NODE.JS API
Authentication
Validation
SQL persistence
POS push state
Logs and WebSockets
The scheduler owns work whose duration is controlled by time and external systems. The backend owns application state and the contracts used by people, the dashboard, and other services.
The Go service loads the configured timezone and cron expression, fetches the previous invoice window from the POS, transforms provider records into the backend contract, and coordinates full and partial retries.
It also exposes a health port, reports job outcomes to the backend, triggers recovery of prior POS failures when safe, synchronizes inventory, and sends alerts after retry exhaustion. None of those operations needs to keep an incoming browser request open.
The TypeScript backend remains the authority for authentication, request validation, SQL models, invoice and product APIs, operation logs, notifications, POS push state, and WebSocket updates.
The scheduler does not write directly into the application's database. It uses authenticated HTTP endpoints, which keeps validation and persistence rules in one place.
HTTP carries commands and results that require a response: bulk invoice creation, job logs, current push status, and retry triggers. Redis Pub/Sub carries scheduler configuration changes, where the important event is that new configuration is available.
Dashboard changes schedule
v
Node.js persists configuration
v
Redis publishes config-changed
v
Go scheduler reloads and reschedules
The scheduler also falls back to fetching configuration from the backend, so Pub/Sub improves responsiveness without becoming the only source of truth.
Docker Compose gives each service its own image, environment, restart policy, health surface, and bounded log files. The API can be deployed for route or dashboard changes without redefining the scheduler loop. The scheduler can restart after an integration fault without taking authenticated application traffic with it.
Separate services also make failure ownership visible. A scheduler health failure means something different from an API health failure, and operations can respond accordingly.
The split adds deployment units, authentication between services, versioned request contracts, network failure handling, and more local setup. A single process would have less wiring.
I accepted that cost because the work already had separate timing, scaling, and failure behavior. Keeping it in one process would hide the boundary without removing it.
Split services by runtime responsibility and failure mode, not by language preference.
Go is useful here because it provides a small, direct scheduler runtime. Node.js is useful because the application and its existing API ecosystem already live there. Neither language is the architecture. The ownership boundary is.
Distributed systems · Realtime reliability
Aura 4 had to commit application state in PostgreSQL and notify connected clients through Centrifugo without letting either system tell a different story.
Decision Commit the state change and its publication intent together, then deliver the event through an independent outbox relay.
Aura 4 uses PostgreSQL as its source of truth and Centrifugo to push changes to connected team members. A completed task, a new handover, or an updated loop must produce one consistent result across the API response, the database, and every active client.
The difficult part is that PostgreSQL and Centrifugo are separate systems. A normal database transaction cannot include a broker publish. Treating those two operations as one step inside a request would hide a dual-write problem rather than solve it.
The simplest implementation would save the business change, publish an event, and return success. That sequence has two failure windows.
The database contains the change, but connected clients never hear about it. Their screens remain stale until another refresh or reconciliation.
Clients receive an event for a change that does not exist. The interface displays ghost state and may build later actions on a false premise.
Retrying the request does not close either window. It can also repeat the database mutation or publish the same event more than once.
I made the database transaction responsible for two records: the business change and an unpublished outbox event describing that change.
Business write + outbox row in one PostgreSQL transaction
If the transaction rolls back, neither record exists. If it commits, the system has durable proof that an event still needs to be delivered. The request no longer depends on Centrifugo being available at that exact moment.
An outbox relay reads unpublished rows, routes each event to the correct channel, publishes it through the broker adapter, and marks the row as published.
Failed publication remains visible as pending work. The relay can retry it without repeating the original business transaction. Because a crash can occur after publishing but before marking the row, delivery and consumers must tolerate duplicate attempts.
This changes the guarantee from an unsafe attempt at exactly-once delivery to a durable, observable at-least-once process.
The outbox adds a relay process, retry behavior, event routing, duplicate tolerance, and ordering rules. It also creates a useful operational boundary.
Publishing directly from the request has less code and can shave time from the happy path. The outbox requires another worker and more operational attention.
I accepted that cost because realtime speed is useful only when the event is true. A slightly later correct update is safer than an immediate event that can disagree with committed state.
Persist the event commitment with the state change. Publish it independently.
Product architecture · Complexity control
A conventional dependency graph promised advance planning, but it also asked teams to maintain speculative relationships before Aura 4 had evidence that the model matched their work.
Decision Record observed blockers as owned Loops and defer dependency inference until real operating history can validate it.
Dependency graphs are familiar in project-management software. A team declares that task B depends on task A, the product draws an edge, and a timeline attempts to predict how delay will move through the plan.
Adding that model to Aura 4 was attractive because it looked like a complete answer to blocked work. It was also a large commitment: dependency types, graph editing, scheduling semantics, visualization, permissions, migration rules, and ongoing maintenance by users.
A dependency view is useful only when its edges are complete and current. Missing relationships hide risk. Stale relationships create false warnings. Both cases make a precise-looking graph less trustworthy than the work it claims to explain.
The graph records what somebody predicted before work started. Aura 4's product problem was more immediate: when work stops, who is waiting, who can respond, what is unresolved, and when did the blockage clear?
Task B waits on task A. The relationship is declared in advance and must remain maintained as the plan changes.
A specific task is blocked now. The record names the person waiting, the person who can act, the reason, and the resolution state.
The Loop captures an operational fact. It does not require the team to model every possible relationship before that relationship affects delivery.
I kept Loops as the first-class blocker model and did not add a general dependency graph. The system records actual interruptions with explicit ownership, then uses those records for stale-loop signals, handovers, and follow-up.
Observe the blockage → assign responsibility → record resolution
This keeps the data close to the event the team experienced. It also makes each record actionable instead of turning it into another line on a planning diagram.
The repository research considered learning structural dependencies from repeated blocker history. That direction could reveal relationships teams consistently encounter without asking them to maintain a graph by hand.
The idea was deliberately postponed. Aura 4's Loop corpus was too young to test whether the inferred relationships would be accurate or useful. Building inference first would create technical sophistication without enough evidence to evaluate it.
The re-evaluation gate is data, not enthusiasm: revisit the idea when multiple real teams have accumulated a meaningful history of closed blocker Loops with clear ownership.
Without declared dependencies, Aura 4 provides less advance timeline forecasting. Teams that need formal critical-path planning may prefer a dedicated planning product.
In return, Aura 4 avoids dependency maintenance, graph visualization, scheduling rules, and a false sense of completeness. The product stays focused on resolving the blockers that are affecting work now.
Model the operational truth before automating the predicted structure.
VI
Templates
N°06 / 07
A polished developer portfolio template with the structure, motion, and visual weight already handled. Replace the content, tune the palette, and launch without rebuilding the whole stage.
New release
Portfolio Pro
$29
One-page storytelling, project tiles, responsive sections, smooth view transitions, and a ready-to-edit static codebase.
M. Abu Salh
From architecture to launch, I build products that ship, scale, and feel right.