<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Zied Hamdi</title>
    <description>The latest articles on Forem by Zied Hamdi (@zhamdi).</description>
    <link>https://forem.com/zhamdi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1024409%2F7ecc1bbe-983c-4ff8-88f9-1cc7e320a076.jpeg</url>
      <title>Forem: Zied Hamdi</title>
      <link>https://forem.com/zhamdi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zhamdi"/>
    <language>en</language>
    <item>
      <title>Rethinking ReBAC: From Accidental Discovery to Zero-Latency Reads</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Sun, 12 Apr 2026 01:44:59 +0000</pubDate>
      <link>https://forem.com/zhamdi/rethinking-rebac-from-accidental-discovery-to-zero-latency-reads-3fgl</link>
      <guid>https://forem.com/zhamdi/rethinking-rebac-from-accidental-discovery-to-zero-latency-reads-3fgl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I needed to solve a common hierarchical permission problem on my project... and unknowingly arrived at ideas that extend Google's Zanzibar paper.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6l1ia99znql87enh0ft.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6l1ia99znql87enh0ft.jpg" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@notethanun?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;note thanun&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/rows-of-colorful-lanterns-in-red-yellow-blue-and-pink-IiCujDqXxSw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was building a mechanism for "setting-spreading". If a parent company upgraded to a premium tier (enabled a specific feature module), or added a user as a manager of some data, that setting needed to automatically cascade down to specific subsidiaries, team members, and/or related entities based on dynamic rules.&lt;/p&gt;

&lt;p&gt;Starting with a single case, the idea was seductive so I extended it to more complex scenarios, before I decided to isolate it as a stand alone solution. Once there, I did my duty of researching whether the problem I was tackling was already solved and open sourced. Surprisingly, it was not so democratized, but it already had a name: ReBAC (Relationship-Based Access Control), formalized by Google's Zanzibar paper.&lt;/p&gt;

&lt;p&gt;But as I explored Zanzibar and its modern SaaS implementations (like SpiceDB or Auth0 FGA), I realized a critical bottleneck: &lt;strong&gt;Latency&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's what I'll cover: the latency problem with standard ReBAC, how AOT materialization solves it, the three abstraction layers that make this engine reusable, and the governance benefits that emerged unexpectedly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Zanzibar Latency Paradigm
&lt;/h2&gt;

&lt;p&gt;Google Zanzibar is a phenomenally robust distributed system designed to compute permissions in real-time. It operates as a centralized authorization engine. (Analogy: OIDC scopes and claims, but resolved live across interrelated services)&lt;/p&gt;

&lt;p&gt;The standard approach for modern ReBAC environments is to operate as centralized microservices relying on Just-In-Time (JIT) graph traversal. When a user requests access to a resource, the engine calculates the permissions over the network.&lt;/p&gt;

&lt;p&gt;This is elegant on paper and adopts a nice microservice SaaS architecture, but in the real world, it becomes a bottleneck:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Network Hops&lt;/strong&gt;: Every &lt;code&gt;isAuthorized&lt;/code&gt; check requires a remote network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-Time Overhead&lt;/strong&gt;: As relationship depth increases, computing the graph at read-time causes latency spikes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your platform needs sub-millisecond read responses, taking a network hit on every single API check quickly becomes a difficult-to-resolve issue without abandoning the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pivot: Spread at Write, Optimize Read
&lt;/h2&gt;

&lt;p&gt;My approach was iterative—I started denormalizing from the first version. The result is a highly flexible &lt;strong&gt;Ahead-Of-Time (AOT) Materialized Engine&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;If a user's rights are inherited by their relationships in the hierarchy, we materialize those rights only once: the moment the relationship changes, not each time he wants to access data. &lt;/p&gt;

&lt;h3&gt;
  
  
  How it Works
&lt;/h3&gt;

&lt;p&gt;Our Materialized ReBAC engine acts as a background hydration pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Graph Traverser&lt;/strong&gt;: When a trigger occurs (e.g., adding an Owner to a Holding Company, or activating a paid module), the engine consults declarative &lt;code&gt;SpreadingConfig&lt;/code&gt; rules. It uses a &lt;code&gt;GraphNavigator&lt;/code&gt; to traverse the Holding-Subsidiaries database relationships. Through a unified interface, implemented in concrete project-specific code, which is easier to test and debug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration Jobs&lt;/strong&gt;: It traverses downstream (or upstream) and injects the resulting permissions or modules directly into the target entities' database records (granting the user inline permissions or activating the paid feature locally). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constant-Time Reads&lt;/strong&gt;: Because the permissions are materialized on the target node, authorization at the API level requires zero mathematical computation or graph traversal. It is a simple object check: &lt;code&gt;entity.modules.includes('ANALYTICS')&lt;/code&gt; or &lt;code&gt;user.roles.includes('OWNER')&lt;/code&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The read latency dropped from network-dependent dozen milliseconds per call (because of network latencies) to absolute zero algorithmic overhead, as it resolves to a db-read that usually is executed along with other data querying in the same network request.&lt;/p&gt;

&lt;h4&gt;
  
  
  Three Abstraction Axes for Project-Agnostic Design
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Graph Navigation Abstraction&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The engine never hardcodes "how to find neighbors." It consumes an injected &lt;code&gt;GraphNavigator&lt;/code&gt; interface that yields adjacent nodes. The engine knows &lt;em&gt;that&lt;/em&gt; traversal happens, not &lt;em&gt;how&lt;/em&gt;—whether across PostgreSQL, GraphQL, or REST endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Policy Decision Abstraction&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The engine delegates all condition evaluations to registered &lt;code&gt;ConditionHandler&lt;/code&gt; plugins. Instead of parsing rigid rule strings, it passes typed entities to project-specific code that answers "yes/no" based on business logic (subscription tiers, payment status, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Action Execution Abstraction&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The engine uses the Command Pattern for state mutation. It receives an &lt;code&gt;Action&lt;/code&gt; payload and blindly calls &lt;code&gt;.execute()&lt;/code&gt;. The engine doesn't know if it's granting a role, activating a module, or firing a webhook—it just runs the command.&lt;/p&gt;




&lt;h4&gt;
  
  
  Testing Advantage: Layered Confidence
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Unit tests on the engine itself&lt;/strong&gt; verify the &lt;em&gt;mechanics&lt;/em&gt;: cycle detection, depth limits, queue ordering, FILO rollback logic, and correlation ID tracking. These run in milliseconds with zero infrastructure—pure Go/Rust/TypeScript logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project-specific plugin tests&lt;/strong&gt; verify the &lt;em&gt;integration&lt;/em&gt;: that your &lt;code&gt;CompanyGraphNavigator&lt;/code&gt; correctly queries subsidiaries, that your &lt;code&gt;SubscriptionConditionHandler&lt;/code&gt; properly evaluates tier levels, and that your &lt;code&gt;RoleMaterializer&lt;/code&gt; writes to the correct database tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beyond reusability, the critical advantage of this design is isolation&lt;/strong&gt;. Because the engine depends only on interfaces, you can test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engine behavior with mock plugins (fast, deterministic, exhaustive)&lt;/li&gt;
&lt;li&gt;Plugin behavior against real databases using project-specific classes and methods. This removes abstraction layers and eases code reading—no unnecessary mental indirection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Beyond Speed: Some Nice Side Effects
&lt;/h2&gt;

&lt;p&gt;But beyond speed, some nice side effects showed up: &lt;strong&gt;governance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because we materialize permissions through an event-driven queue, we can safely emulate (dry-run) outcomes before applying them. This allows for governance patterns that are traditionally difficult to achieve in JIT engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform-Style Dry Runs &amp;amp; Pending Changes:&lt;/strong&gt; By treating permissions as a queue of upcoming events, we can simulate massive cascades and stage "Pending Changes" before they are committed (navigating the tree again to activaite nodes on payment success). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Human in the Middle":&lt;/strong&gt; An administrator can review the simulated propagation tree and explicitly lock specific entities or deactivate spreading on specific modules &lt;em&gt;before&lt;/em&gt; executing the materialization. It turns authorization into a clear &lt;strong&gt;Permission Staging&lt;/strong&gt; pipeline that can be submitted for review before getting materialized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Correlation &amp;amp; Rollbacks:&lt;/strong&gt; Every wave of spreading is tied to a &lt;code&gt;correlationId&lt;/code&gt;. If an upstream change is undone (or a human makes a mistake), the engine can perfectly roll back the entire materialized state across the tree. Repeatedly in a FILO (first in, last out) manner. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision Transparency:&lt;/strong&gt; Every time the engine &lt;em&gt;refuses&lt;/em&gt; to spread a module down a valid path (due to failing a conditional check), it records the detailed structural reasoning in a unified &lt;code&gt;AuditLog&lt;/code&gt; for better understanding of the reasons of refusal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cycle &amp;amp; Depth Safety:&lt;/strong&gt; To prevent infinite loops, the queue maintains a strict &lt;code&gt;pathHistory&lt;/code&gt; for cycle detection and enforces per-operation-type configuration-specific depth limits to stop cascade exhaustion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solving for these edge cases effectively blurred the line between a basic Policy Engine and a full-fledged Identity Governance Administration (IGA) system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Zanzibar definitively proved that static roles are dead and relationships are the future of access control. &lt;/p&gt;

&lt;p&gt;But adopting ReBAC doesn't mean you have to surrender your application to the latency of external API computations. By embracing AOT Materialization, it is possible to build a generic, extensible engine that guarantees the dynamic power of relationship-based access with the uncompromising speed of local read queries. &lt;/p&gt;

</description>
      <category>rebac</category>
      <category>architecture</category>
      <category>zanzibar</category>
      <category>oidc</category>
    </item>
    <item>
      <title>Svelter Monthly Awards: January 2026 Winners</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Tue, 17 Feb 2026 17:38:31 +0000</pubDate>
      <link>https://forem.com/zhamdi/svelter-monthly-awards-january-2026-winners-52b</link>
      <guid>https://forem.com/zhamdi/svelter-monthly-awards-january-2026-winners-52b</guid>
      <description>&lt;h1&gt;
  
  
  🏆 Svelter Monthly Awards: January 2026
&lt;/h1&gt;

&lt;p&gt;Today is a big day for &lt;a href="https://svelter.me/" rel="noopener noreferrer"&gt;Svelter&lt;/a&gt; and, I hope, for the Svelte community. After three months of background metric collection and performance analysis, I am finally ready to announce the &lt;strong&gt;first-ever batch of Svelter Monthly Winners!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Winner and why did it take three months?
&lt;/h2&gt;

&lt;p&gt;Before we look at the results, you might wonder what exactly a "winner" is on this platform, how it is computed, and why it took three months to determine a single month's results.&lt;/p&gt;

&lt;p&gt;On Svelter, a winner isn't just the project with the biggest numbers. To stay true to the mission of detecting &lt;strong&gt;momentum&lt;/strong&gt; without ignoring &lt;strong&gt;stability&lt;/strong&gt;, Svelter uses a three-axis scoring system that requires three months of data to mature:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Speed (Growth Rate):&lt;/strong&gt; The percentage growth of metrics during the current month. This includes both external signals (GitHub stars, NPM downloads) and community engagement within Svelter (upvotes, favorites, comments).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Acceleration:&lt;/strong&gt; The change in growth rate compared to the previous month. Svelter rewards projects that are "gaining heat."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Size Factor (The "Stability" axis):&lt;/strong&gt; We don't just look at percentages. Svelter also factors in raw volume using a logarithmic scale. This ensures that massive, foundational projects aren't put aside just because their growth percentage is naturally lower than a brand-new project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What makes this scoring unique is how it weighs different signals. While GitHub stars can be ambiguous (is it a bookmark? a thank you? a "maybe later"?) and NPM downloads often come from dependency trees rather than direct choice, community actions like &lt;strong&gt;upvotes&lt;/strong&gt; have one clear purpose: to thank the author. Similarly, &lt;strong&gt;favorites&lt;/strong&gt; signal a deliberate intent to return. These intentional signals receive special weight in the algorithm.&lt;/p&gt;

&lt;p&gt;By calculating these three physics of growth, Svelter ensures that winners are projects that are both &lt;strong&gt;historically significant&lt;/strong&gt; and &lt;strong&gt;currently trending&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: January winners are displayed in February because the month must fully conclude before all downloads and stars can be tallied and verified.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Svelter Vision: A Meritocracy for Svelte
&lt;/h2&gt;

&lt;p&gt;This rigorous scoring isn't just about math; it's about fulfilling Svelter's broader vision of bringing a true meritocracy to the ecosystem. I built this platform to make it easier for developers to navigate the sea of options. Svelter is designed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Discover Adoption:&lt;/strong&gt; Find what the community is actually using through &lt;a href="https://svelter.me/month/01-2026?cat_key=state-management" rel="noopener noreferrer"&gt;category-specific&lt;/a&gt; trends.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reward Merit:&lt;/strong&gt; Give visibility to contributors who grow the community, whether they are writing libraries, blog articles, or even just helpful comments.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Detect Trending Stacks:&lt;/strong&gt; Use the &lt;strong&gt;Echolinks&lt;/strong&gt; feature to surface emerging tech stacks before they go mainstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you are looking for a library by &lt;a href="https://svelter.me/explore?tag=email" rel="noopener noreferrer"&gt;tag&lt;/a&gt; or a specific &lt;a href="https://svelter.me/explore?q=email" rel="noopener noreferrer"&gt;keyword&lt;/a&gt;, the goal is to provide a clear path to the most relevant tools.&lt;/p&gt;

&lt;p&gt;Central to this meritocracy is &lt;strong&gt;permanence&lt;/strong&gt;. Every monthly winner is graved in Svelter's history, creating a lasting record of community momentum. I am currently developing a feature that will allow winners to display their trophies directly on their own pages and content—turning these achievements into visible badges of recognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evolution: Winners vs. Explore Mode
&lt;/h2&gt;

&lt;p&gt;Translating this vision into a usable product has been a learning experience. When I started building Svelter, I originally only developed the &lt;strong&gt;Explore mode&lt;/strong&gt;. However, I noticed a painful &lt;strong&gt;80% bounce rate&lt;/strong&gt;. I realized that the effort of finding best performers through the scoring algorithm wasn't immediately visible to new visitors.&lt;/p&gt;

&lt;p&gt;To solve this, I added the &lt;strong&gt;Winners display mode&lt;/strong&gt; to expose these results clearly and made it the default view. My goal is to make it immediately obvious who is trending. That said, I've heard that the entry page can still be a bit unclear on first arrival—&lt;strong&gt;I would love your suggestions on how to improve the layout or clarity!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Podium: Overall Winners
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fme3iso64csy6syc7aaka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fme3iso64csy6syc7aaka.png" alt=" " width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After refining how we display these results, let's see which projects actually claimed the top spots for January. You can see the full interactive podium at &lt;a href="https://svelter.me/month/01-2026" rel="noopener noreferrer"&gt;svelter.me/month/01-2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A perfect illustration of our 3-axis logic is the &lt;strong&gt;"Supabase Paradox"&lt;/strong&gt;: &lt;code&gt;supabase-js&lt;/code&gt; recorded &lt;strong&gt;24.3 million downloads&lt;/strong&gt; this month. Thanks to its &lt;strong&gt;Size Factor&lt;/strong&gt;, it maintained a strong &lt;strong&gt;5th place&lt;/strong&gt; despite a 17.8% growth rate. However, it was overtaken by projects like &lt;code&gt;better-svelte-email&lt;/code&gt;, which saw a staggering &lt;strong&gt;109% growth rate&lt;/strong&gt;. Svelter balances raw power with explosive momentum.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥇 1st Place: &lt;a href="https://github.com/Konixy/better-svelte-email" rel="noopener noreferrer"&gt;better-svelte-email&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; &lt;a href="https://svelter.me/month/01-2026?cat_key=boilerplates-starters" rel="noopener noreferrer"&gt;Boilerplates &amp;amp; Starters&lt;/a&gt;&lt;br&gt;
With its massive growth rate and acceleration, it has claimed the first-ever Gold.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥈 2nd Place: &lt;a href="https://github.com/huntabyte/shadcn-svelte" rel="noopener noreferrer"&gt;shadcn-svelte&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; &lt;a href="https://svelter.me/month/01-2026?cat_key=ui-components" rel="noopener noreferrer"&gt;UI Components&lt;/a&gt;&lt;br&gt;
A community titan that continues to accelerate. Notably, it saw a &lt;strong&gt;300% surge in community upvotes&lt;/strong&gt;—a clear signal that developers are actively recognizing its value on Svelter.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥉 3rd Place: &lt;a href="https://github.com/wobsoriano/svelte-sonner" rel="noopener noreferrer"&gt;svelte-sonner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; &lt;a href="https://svelter.me/month/01-2026?cat_key=notifications-modals" rel="noopener noreferrer"&gt;Notifications &amp;amp; Modals&lt;/a&gt;&lt;br&gt;
The gold standard for toasts, seeing over &lt;strong&gt;283K fresh downloads&lt;/strong&gt; in January alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Personal Note:&lt;/strong&gt; I'm also thrilled to share that my own library, &lt;a href="https://svelter.me/month/01-2026?cat_key=misc" rel="noopener noreferrer"&gt;user-credits-core&lt;/a&gt;, claimed 2nd place in the &lt;a href="https://svelter.me/month/01-2026?cat_key=misc" rel="noopener noreferrer"&gt;Miscellaneous category&lt;/a&gt;. This three-year-old project had very slow growth initially, leading me to leave it unmaintained. But seeing renewed interest this month—especially knowing these downloads represent genuine adoption rather than dependency tree noise—has inspired me to revisit it. It's a reminder that momentum can reignite even for older projects when the right community finds them.&lt;/p&gt;




&lt;p&gt;Congratulations to all the maintainers. Your hard work is what makes our ecosystem thrive. 🚀&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you think the platform could be improved, please write it in the comments, I'd love to hear from you.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>awards</category>
      <category>sveltekit</category>
      <category>svelte</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>Running AirLLM Locally on Apple Silicon: Not So Good</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Wed, 28 Jan 2026 11:20:07 +0000</pubDate>
      <link>https://forem.com/zhamdi/running-airllm-locally-on-apple-silicon-not-so-good-2f0f</link>
      <guid>https://forem.com/zhamdi/running-airllm-locally-on-apple-silicon-not-so-good-2f0f</guid>
      <description>&lt;p&gt;This week, armed with an article on huggingface talking about &lt;a href="https://huggingface.co/blog/lyogavin/airllm" rel="noopener noreferrer"&gt;how AirLLM can run 70b models on 4GB of GPU&lt;/a&gt;, I thought. my M4 MacBook Pro (48GB RAM) could run a local coding assistant. Especially that Kilo Code discontinued the free xGrog AI, the timing was as falling from the sky.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;Create a local xGrog replacement after xGrog became paid on Kilo code. I wanted to run models like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CodeLlama 13B/70B&lt;/li&gt;
&lt;li&gt;DeepSeek-OCR models
&lt;/li&gt;
&lt;li&gt;Llama 3.1 variants&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Funny fact: I didn't have to type all the commands and python files by hand, just asking deepseek to do it for me. And giving directions on failures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Actually Worked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Success: AirLLM with Mistral 7B
&lt;/h3&gt;

&lt;p&gt;Using AirLLM's MLX backend, I successfully ran Mistral 7B without quantization. The key was proper PyTorch-to-MLX tensor conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First, upgrade pip&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip

&lt;span class="c"&gt;# run python in virtualized env&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv  
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate 

&lt;span class="c"&gt;# Install airllm&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;airllm

&lt;span class="c"&gt;# Install PyTorch for Mac (MPS support)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;torch torchvision torchaudio

&lt;span class="c"&gt;# Optional: for better performance&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;accelerate

&lt;span class="c"&gt;# had to add these (compared to official docs)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;mlx-lm 
pip &lt;span class="nb"&gt;install &lt;/span&gt;transformers
pip &lt;span class="nb"&gt;install &lt;/span&gt;safetensors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# airllm_working_mac.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airllm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModel&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mlx.core&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mx&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚀 AirLLM on Apple Silicon - Working Version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load model
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mistralai/Mistral-7B-Instruct-v0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ Model loaded successfully&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generate a response using AirLLM on Mac&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Tokenize
&lt;/span&gt;    &lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;return_attention_mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Convert PyTorch → NumPy → MLX
&lt;/span&gt;    &lt;span class="n"&gt;pt_tensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input_ids&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;numpy_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pt_tensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mlx_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numpy_array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📝 Input length: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mlx_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Generate - returns STRING directly
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;mlx_array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_new_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;use_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Clean up the response (remove extra end tokens)
&lt;/span&gt;    &lt;span class="n"&gt;cleaned_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/s&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cleaned_response&lt;/span&gt;

&lt;span class="c1"&gt;# Test it
&lt;/span&gt;&lt;span class="n"&gt;test_prompts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the capital of France?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a Python function to calculate factorial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain quantum computing in simple terms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_prompts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🤖 Response:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Save to file
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ All tests completed! Responses saved to response_*.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Every question took ~10 minutes, subsequent runs were not faster. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3aho19w5yu0ag2nfcem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3aho19w5yu0ag2nfcem.png" alt="AirLLM takes time on each question" width="800" height="1045"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What about bigger models?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Quantization Woes
&lt;/h3&gt;

&lt;p&gt;The biggest limitation: &lt;strong&gt;bitsandbytes doesn't work on Mac&lt;/strong&gt;. This meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No 4-bit compression&lt;/strong&gt; for larger models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;48GB RAM limits&lt;/strong&gt; us to ~13B models max without quantization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mlx-community models&lt;/strong&gt; (pre-quantized) had loading issues
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This fails on Mac - bitsandbytes needs CUDA
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Phind-CodeLlama-34B-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4bit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Error: "Torch not compiled with CUDA enabled"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ❌ File Format Frustrations
&lt;/h3&gt;

&lt;p&gt;Downloaded &lt;code&gt;mlx-community/CodeLlama-13b-Python-4bit-MLX&lt;/code&gt; (7GB) but hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FileNotFoundError: No safetensors found in [path]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pre-converted MLX models didn't match MLX-LM's expected file structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Performance Reality
&lt;/h3&gt;

&lt;p&gt;Even with successful 7B models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8.5 minutes&lt;/strong&gt; for initial AirLLM layer splitting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Significant disk space&lt;/strong&gt; needed for cache (20+ GB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory pressure&lt;/strong&gt; even with "small" models&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture Limitation
&lt;/h2&gt;

&lt;p&gt;Apple's MLX framework is promising but immature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No native quantization&lt;/strong&gt; support (relies on bitsandbytes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model ecosystem&lt;/strong&gt; is spotty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation gaps&lt;/strong&gt; for edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion:
&lt;/h2&gt;

&lt;p&gt;Hosted Solutions Still Win for now. The Mac Studio (M3 Ultra, 192GB RAM) looks like a good silent AI workstation at home. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same quantization issues&lt;/strong&gt; apply&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still limited&lt;/strong&gt; by MLX ecosystem maturity
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: $7,000+ for hardware vs $20/month cloud&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AirLLM&lt;/strong&gt; has significant run overhead, a very "smart" model could compensate for the loading time by executing a complex task, but there are many bottelnecks to only getting it running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Silicon needs&lt;/strong&gt; native quantization tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ecosystem&lt;/strong&gt; is moving fast but isn't production-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For serious work&lt;/strong&gt;, cloud solutions still dominate&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What else I tried on my Mac?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ollama with smaller models&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ollama
ollama run codellama:13b-python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It just works. No tensor conversions, no quantization headaches, but is very slow: of no help in the real world tasks, and killed memory consumption. &lt;br&gt;
(there are articles out there on how to run it, if you're curious)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;So the biggest challenge to learn AI model manipulation today is the hardware entry ticket.&lt;br&gt;
Any alternatives you tried on your side? Tell me in the comments &lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>airllm</category>
      <category>m4</category>
      <category>mlx</category>
      <category>huggingface</category>
    </item>
    <item>
      <title>Bundled Offers with user-credits npm</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Tue, 05 Dec 2023 13:34:53 +0000</pubDate>
      <link>https://forem.com/zhamdi/bundled-offers-with-user-credits-npm-1pbd</link>
      <guid>https://forem.com/zhamdi/bundled-offers-with-user-credits-npm-1pbd</guid>
      <description>&lt;h1&gt;
  
  
  Unveiling New Features: Bundled Offers and Deduction of Expired Credits
&lt;/h1&gt;

&lt;p&gt;In my continuous effort to enhance &lt;a href="http://user-credits.dev" rel="noopener noreferrer"&gt;user-credits&lt;/a&gt;, I'm excited to introduce two groundbreaking features that bring the library to its 1.0.0-beta launch: Bundled Offers and the Deduction of Expired Credits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feegaixn26bz2h24tuwcl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feegaixn26bz2h24tuwcl.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundled Offers: Simplifying Multi-Offer Management
&lt;/h2&gt;

&lt;p&gt;In the first versions of the &lt;a href="http://user-credits.dev" rel="noopener noreferrer"&gt;user-credits&lt;/a&gt; lib, managing multiple offers simultaneously has been a challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to process multiple orders in a single transaction?&lt;/li&gt;
&lt;li&gt;How to monitor service consumption individually?&lt;/li&gt;
&lt;li&gt;Which services can be extended with other offers and which cannot?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the library was able to manage multiple offers, with different tokens and dates, but there was no way to take multiple offers and make out a new single offer from them.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Bundled Offers Work
&lt;/h3&gt;

&lt;p&gt;Offers can now be combined, enabling the creation of packages similar to those offered by mobile service providers. Picture offering users diverse packages for $20, $50, and $100/month, proposing different quotas of calling hours, mobile data, and additional services like mobile TV or games—all seamlessly combined.&lt;/p&gt;

&lt;p&gt;Now, another question arises: if a user buys a $20 package, what happens if they then purchase a $50 one? Or if they want to extend their calling quota only without adding data, are they allowed to do that? Does it extend the expiry date of the calls?&lt;/p&gt;

&lt;p&gt;Briefly delving into "Offer Design" in this article, for a more in-depth understanding of how offers work, check &lt;a href="https://github.com/ziedHamdi/user-credits/blob/master/docs/offers_explained.md" rel="noopener noreferrer"&gt;this one&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We only need to create a few basic offers, then create the bundles pointing to them by their &lt;code&gt;_id&lt;/code&gt;. For the example of the mobile operator, the basic offers would be &lt;code&gt;Call&lt;/code&gt; where a token is one minute, &lt;code&gt;Data&lt;/code&gt; where a token is one Kb, and &lt;code&gt;TV&lt;/code&gt; where there are no tokens, only an expiry date.&lt;/p&gt;

&lt;p&gt;From that, we could create the bundled offers, such as the $20 bundled offer proposing 4 hours (quantity= 4 x 60 units) of &lt;code&gt;Calls&lt;/code&gt; and 500Mb (quantity= 500 x 1024 units of Kb) of &lt;code&gt;Data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;IOffer&lt;/code&gt; now has a &lt;code&gt;combinedItems&lt;/code&gt; field, which is an array of ICombinedOffer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ICombinedOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;IMinimalId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root offer decides on the expiry date of all the bundled suboffers through its cycle field. But suboffers have the possibility to get their expiry date extended if the offer allows it.&lt;/p&gt;

&lt;p&gt;To do that, the field &lt;code&gt;appendDate&lt;/code&gt; was introduced; here's its JSDoc which explains it all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Determines how the expiry date is handled at the time of purchase.
 *
 * If {@link IOrder.starts} is not null, no algorithm is executed: it is used as-is, unless the date has passed, in which case, an error occurs.
 *
 * If set to true:
 * - The expiry date extends from the current expiry date of the same offerGroup in {@link IUserCredits.offers[offerGroup]}.
 * - The extension is by the duration specified in {@link IOffer.cycle}.
 *
 * If set to false:
 * - Otherwise, {@link Date.now()} is used as the start date.
 * - The expiry date is calculated by adding {@link IOffer.cycle} to the start date.
 *
 * When the expiry date is reached:
 * - Remaining tokens are deducted from the offerGroup.
 *
 * The computation of remaining tokens:
 * - SUM of tokens from {@link ITokenTimeTable} for the period between start and expires.
 * - This includes added tokens at creation minus all consumptions during that period.
 *
 * As the date expires and nothing can be appended to it:
 * - Unused tokens from that purchase are removed.
 * NOTE1: the field {@link IOrder.quantity} will always multiply {@link IOffer.cycle} to compute the final expiry date.
 * NOTE2: It's not recommended to mix offers with different appendDate values in the same "offerGroup" as it can mislead users.
 */&lt;/span&gt;
&lt;span class="nx"&gt;appendDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-Time Credit Tracking
&lt;/h3&gt;

&lt;p&gt;Developers can effortlessly track their users' credit balances and consumption for each bundled offer by calling a single function. Whether checking calling minutes, data usage, or entertainment credits, the new feature provides clear visibility. Additionally, it's through this function that expired orders are deactivated, and their corresponding credits are deducted from the user balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduction of Expired Credits: Streamlining Credit Management
&lt;/h2&gt;

&lt;p&gt;Efficient credit management involves handling expired credits seamlessly. The &lt;strong&gt;Deduction of Expired Credits&lt;/strong&gt; feature streamlines the process, ensuring users are promptly informed (warning them in advance and logging all operations) and credits are deducted seamlessly. As every token operation is saved in the &lt;code&gt;ITokenTimetable&lt;/code&gt; collection, and as different offers can be grouped in the same bag when they have the &lt;code&gt;offerGroup&lt;/code&gt; value, consuming tokens is done without knowing which exact order is targeted. But don't worry, we can decide upon that when the time comes.&lt;/p&gt;

&lt;p&gt;This is the signature of a token consumption call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;tokensConsumed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;


  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the user that consumed tokens&lt;/span&gt;
  &lt;span class="nx"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the offerGroup that the user consumed from&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the count of tokens consumed&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ITokenTimetable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each call to this method creates a line, saving the date and the consumed tokens as a negative number in the &lt;code&gt;ITokenTimetable&lt;/code&gt; collection. Conversely, each (successful) purchase of any offer triggers a line with a positive number in the same collection. This happens when you call &lt;code&gt;afterExecute&lt;/code&gt;: it first contacts the payment gateway to read the state of the payment. If it was paid, multiple operations happen, and among them, a line is added with that date to &lt;code&gt;ITokenTimetable&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;IService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;afterExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IUserCredits&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So when an order expires, we know how many tokens it added to the &lt;code&gt;offerGroup&lt;/code&gt; basket, we also know when it started and when it expired. So computing how many tokens of that order were consumed in the allocated time translates to summing up negative entries from &lt;code&gt;ITokenTimetable&lt;/code&gt; between the two dates.&lt;/p&gt;

&lt;p&gt;An important rule is that orders are kept unchanged from the moment their status becomes "paid". We consider the order entry an archive of what exactly happened: it also copies information from the offer to be able to keep the history clean if ever offers come to change (which is something we don't advise doing, we recommend creating a new offer with the same &lt;code&gt;offerGroup&lt;/code&gt; instead). The only thing that can change in an order is its status, and that happens only a few times: before the payment is validated, when the payment fails, or is validated, and when the expiry date hits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that when the expiry date of an order hits, nothing happens automatically. I decided to let the developer choose when is the opportune time to call the &lt;code&gt;checkForExpiredOrders&lt;/code&gt; function. The function does all the work for you. However you have to adopt a strategy on when to call it: it can be a cron that runs every day once for all users, it can be a table that stores the dates of the soon expiring orders and calls those that expired immediately when they end. You name it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Warnings for Low Credits and Imminent Expiry
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uniqueUserId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;warningDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 7 days in milliseconds&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lowLimits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calls&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warnings&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkForExpiredOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warningDuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lowLimits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not only does this function clean the database from the trailing tokens, but it also empowers developers with timely warnings about low credits and imminent expiry of offers. The &lt;code&gt;checkForExpiredOrders&lt;/code&gt; function allows users to set warnings for durations and low token levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With Bundled Offers and the Deduction of Expired Credits, we're ushering in a new era of simplicity and effectiveness in credit management. These features not only enhance user experience but also open doors to innovative pricing models and subscription plans.&lt;/p&gt;

&lt;p&gt;Ready to elevate your credit management game? Host a user-credits library today in a side docker VM and start experimenting without any risks. You might end-up having the credit management as your first micro-service in your app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article has been added to &lt;a href="https://github.com/ziedHamdi/user-credits-core/blob/master/docs/bundled_offers_and_credit_purging.md" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; of the project&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>node</category>
      <category>startup</category>
      <category>stripe</category>
    </item>
    <item>
      <title>Ready to use components for credits and payment flow</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Wed, 25 Oct 2023 14:26:47 +0000</pubDate>
      <link>https://forem.com/zhamdi/ready-to-use-components-for-credits-and-payment-flow-4n0h</link>
      <guid>https://forem.com/zhamdi/ready-to-use-components-for-credits-and-payment-flow-4n0h</guid>
      <description>&lt;h1&gt;
  
  
  Unlocking the Power of UserCredits: Making Payments a Breeze for Small Startups
&lt;/h1&gt;

&lt;p&gt;So, you have a brilliant startup idea. You're all set to create the next big thing, but there's a catch: you need to handle payments and credit management. That's where UserCredits comes to the rescue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that this is still work in progress, but I wrote this article because I'm eager to talk to people who would like to contribute both on &lt;a href="https://github.com/ziedHamdi/UserCredits" rel="noopener noreferrer"&gt;user-credits&lt;/a&gt; and &lt;a href="https://github.com/ziedHamdi/svelte-user-credits" rel="noopener noreferrer"&gt;user-credits-ui&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Birth of UserCredits
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;It all started when I was building my project, &lt;a href="https://cvlink.in/" rel="noopener noreferrer"&gt;CvLink.in&lt;/a&gt;, an AI resume-building platform. I needed a payment system with credit management, but I couldn't find a library tailored to the needs of small startups like mine. So, I took matters into my own hands.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1sq694mqp9if258akvp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1sq694mqp9if258akvp.jpg" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Solution for the Uncharted Territory
&lt;/h2&gt;

&lt;p&gt;UserCredits is not your run-of-the-mill payment library. It's a versatile, technology-agnostic solution designed to adapt to any front-end framework, making it accessible to developers of all backgrounds. It provides the building blocks for handling payments, offers, and user credits with ease and flexibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unveiling User-Credits-UI: Where the Magic Happens
&lt;/h3&gt;

&lt;p&gt;Now, let's talk about &lt;strong&gt;user-credits-ui&lt;/strong&gt;, the enchanting front-end layer of UserCredits. This part of the library brings a touch of magic to your project, and I promise, it's more fun than reading an academic paper.&lt;/p&gt;

&lt;p&gt;Imagine you have an array of offers and you want to display them in a visually appealing way. With user-credits-ui, it's as easy as waving a magic wand. Let's take a look at how we can represent prices using Svelte, but remember, it's adaptable to any front-end technology.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// +layout.svelte&lt;/span&gt;
&lt;span class="c1"&gt;// Set up your resolver and element builder&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OfferImpl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/example/impl/model/OfferImpl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Pricing&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/components/Pricing.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Mocking values that normally come from the user-credits lib&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;freeOffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startupOffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;startup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scaleupOffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;002&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scaleup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enterpriseOffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;003&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enterprise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pricing&lt;/span&gt; &lt;span class="nx"&gt;offerList&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="nx"&gt;freeOffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startupOffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scaleupOffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enterpriseOffer&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Pricing is a provided template that creates &lt;code&gt;&amp;lt;Offer&amp;gt;&lt;/code&gt; objects. These objects use the resolver to determine how to represent the offers. It uses templates from the very interesting &lt;a href="https://preline.co/examples/pricing-sections.html" rel="noopener noreferrer"&gt;preline&lt;/a&gt; project that is built on top of &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;tailwindcss&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The magic behind the scenes is the &lt;strong&gt;Resolver&lt;/strong&gt;, an essential part of UserCredits. It transforms raw data into view specifications, ensuring your data is displayed exactly as you want it. Here's a sneak peek into the &lt;code&gt;Resolver&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Resolver class&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Resolver&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;IResourceResolver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;getObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;IGeneratorData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IResourceDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Implement your logic here: for example&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;IOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, meet &lt;strong&gt;OfferProps&lt;/strong&gt;. It takes each offer, identifies it, and completes its data. Here's how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OfferProps class&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OfferProps&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;IOfferProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// a static switch case could do the trick&lt;/span&gt;
        &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValuePresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// you can add css or tailwind css for example&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValuePresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forever free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-red-500 font-bold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValuePresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// and so on for other cases&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ValuePresentation&lt;/strong&gt; has a constructor with optional parameters for customizing CSS, providing additional flexibility. The &lt;code&gt;getObject&lt;/code&gt; method in the &lt;strong&gt;Resolver&lt;/strong&gt; is an opportunity to enrich the view with elements specific to your platform or add data not available in the &lt;code&gt;IOffer&lt;/code&gt; object coming from the &lt;a href="https://github.com/ziedHamdi/UserCredits" rel="noopener noreferrer"&gt;UserCredits&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;With UserCredits and &lt;a href="https://github.com/ziedHamdi/user-credits-ui" rel="noopener noreferrer"&gt;user-credits-ui&lt;/a&gt; (now &lt;a href="https://github.com/ziedHamdi/svelte-user-credits" rel="noopener noreferrer"&gt;for svelte&lt;/a&gt;), you can represent offers, payments, and user credits in a straightforward way that adapts to any front-end framework. It's like having your own magical toolkit for creating rich, dynamic views without breaking a sweat.&lt;/p&gt;

&lt;p&gt;Whether you're building the next big startup or a passion project, UserCredits has your back. It's time to embrace the magic and make payments a breeze for your small startup.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To learn more about UserCredits and explore the possibilities, check out the &lt;a href="https://github.com/ziedHamdi/UserCredits" rel="noopener noreferrer"&gt;UserCredits GitHub repository&lt;/a&gt;. And its view layer &lt;a href="https://github.com/ziedHamdi/svelte-user-credits" rel="noopener noreferrer"&gt;svelte-user-credits&lt;/a&gt; that will be moved to &lt;a href="https://github.com/ziedHamdi/user-credits-ui" rel="noopener noreferrer"&gt;user-credits-ui&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>billing</category>
      <category>payment</category>
      <category>startup</category>
      <category>lib</category>
    </item>
    <item>
      <title>Pay-as-you-go, A Flexible Approach to Managing Multiple Offers Simultaneously for a user</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Wed, 11 Oct 2023 19:59:59 +0000</pubDate>
      <link>https://forem.com/zhamdi/pay-as-you-go-a-flexible-approach-to-managing-multiple-offers-simultaneously-for-a-user-2a1m</link>
      <guid>https://forem.com/zhamdi/pay-as-you-go-a-flexible-approach-to-managing-multiple-offers-simultaneously-for-a-user-2a1m</guid>
      <description>&lt;h1&gt;
  
  
  A Flexible Approach to Managing Multiple Offers Simultaneously
&lt;/h1&gt;

&lt;p&gt;Hey there, I'm Zied Hamdi, the creator behind &lt;a href="https://github.com/ziedHamdi/UserCredits" rel="noopener noreferrer"&gt;UserCredits&lt;/a&gt;, and I'm excited to introduce you to the new approach for managing multiple offers and subscriptions simultaneously. As a developer, I understand the importance of flexibility and customization, and I'm here to show you how I've made it a reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/zhamdi/a-pay-as-you-go-library-is-born-38jn"&gt;original model&lt;/a&gt;, users were restricted to a single subscription plan. This meant that if you wanted to subscribe to different services or enjoy multiple offers, you were out of luck. That one-size-fits-all approach would have been limiting and frustrating for both users and developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Approach
&lt;/h2&gt;

&lt;p&gt;The innovation introduces the concept of "offer groups" to give users the flexibility they need. Here's how it works:&lt;/p&gt;

&lt;h3&gt;
  
  
  Offer Groups
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Each offer is associated with an "offer group." An offer group is a way to categorize related offers. For example, you might have an offer group for "Mobile TV."
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g., "Mobile TV"&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multiple Subscriptions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;With this new approach, users can subscribe to multiple offers and services independently.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ISubscription&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g., "Mobile TV"&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Personalization
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Users can customize their subscription experience to suit their needs. Whether it's mixing and matching different offers or choosing different durations, the power is in the user's hands.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mobileTvSubscription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISubscription&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mobile TV&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phoneAppSubscription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISubscription&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;offerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Phone App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tracking Expiry Dates and Tokens
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Each offer group has its own set of expiry dates and tokens. This ensures that your "Mobile TV" subscription doesn't interfere with your "Phone App" subscription.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingOffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IActivatedOffer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userCredits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offerGroup&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offerGroup&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enhanced User Experience
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;This new approach offers an enhanced user experience. It simplifies the management of subscriptions and allows users to enjoy various services with pay-as-you-go balance real time monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Advantages
&lt;/h2&gt;

&lt;p&gt;The advantages of this new approach are crystal clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Users can subscribe to multiple offers and services simultaneously, tailoring their experience to their preferences.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customization&lt;/strong&gt;: The ability to mix and match offers and durations ensures a personalized subscription experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simplified Management&lt;/strong&gt;: Offer groups make it easy to keep track of various subscriptions, tokens, and expiry dates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enhanced User Experience&lt;/strong&gt;: Users have the freedom to enjoy a diverse range of services without constraints.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;I invite you to explore this new approach and discover the difference it can make for your users. You can check out the full code and implementation details on &lt;a href="https://github.com/ziedHamdi/UserCredits" rel="noopener noreferrer"&gt;UserCredits&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;Feel free to customize and contribute to the project, as I welcome any input and collaboration.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zied Hamdi&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Javascript: # How to Solve Async IoC Paradox</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Fri, 06 Oct 2023 11:50:19 +0000</pubDate>
      <link>https://forem.com/zhamdi/javascript-how-to-solve-async-ioc-paradox-he3</link>
      <guid>https://forem.com/zhamdi/javascript-how-to-solve-async-ioc-paradox-he3</guid>
      <description>&lt;h1&gt;
  
  
  How to Solve the IoC Infinite Dependency Paradox in JS and TS
&lt;/h1&gt;

&lt;p&gt;In the world of advanced software development, managing dependencies efficiently is crucial. This often involves using Inversion of Control (IoC) containers like Awilix to handle the instantiation and management of objects. While IoC containers are powerful tools for dependency management, one common challenge is ensuring that certain objects, like your IoC container itself, remain singletons, meaning that there is only one instance throughout your application's lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IoC Paradox
&lt;/h2&gt;

&lt;p&gt;Imagine you're building a complex application using an IoC container to manage dependencies, and you need to create an IoC container with various registrations for your services and components. Ideally, you want to create this container only once and use it consistently throughout your application to ensure that the same instances are injected wherever needed. However, there's an issue: you need to initialize the container asynchronously, as many operations like database connection pool is started as asynchronous operations.&lt;/p&gt;

&lt;p&gt;The paradox is that you cannot use IOC for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wider Problem
&lt;/h2&gt;

&lt;p&gt;The IoC paradox is not limited to Awilix; it extends to many IoC libraries. The challenge lies in handling asynchronous calls during registration or resolving of dependencies. Some libraries may not provide straightforward solutions for this, leaving developers to devise their own strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Solution 1: Export the Container
&lt;/h3&gt;

&lt;p&gt;One approach is to export the container instance from the module where it's created and import it wherever needed. However, this will still require you to call the container initializer async method somewhere in your app, and nothing will prevent other developers from calling it again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: Initialize then container and Store in global
&lt;/h3&gt;

&lt;p&gt;Another approach is to initialize the container only once and put it in the global context. While this centralizes container management, it still requires an initial call somewhere in an invasive place in your program to initialize the container. Then storing the value in the global scope is a bad practice for many reasons that I will not detail here. But just think about anyone overriding your instance for example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 3: Use Dependency Injection
&lt;/h3&gt;

&lt;p&gt;Dependency injection can be employed to pass the container to components and services where it's needed. While this approach maintains the benefits of IOC, it requires an initialization outside the original container file, as it has to be called in async mode (which is not supported by the IOC containers). So we are getting back to the polluting place where we do routines that have to be done before anything relying on the container can load.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Elegant Solution: The Singleton Pattern
&lt;/h2&gt;

&lt;p&gt;To address these issues, we introduce an elegant solution: the Singleton Pattern for IoC containers. This pattern ensures that the container is created and initialized only once while providing a clean and efficient way to access it without manual storage.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
typescript
import { asFunction, asValue, createContainer } from "ioc-library";

export class IoCContainerSingleton {
  private static container: IoCContainer&amp;lt;object&amp;gt;;

  private constructor() {
    // Private constructor to prevent external instantiation
  }

  public static async getInstance(): Promise&amp;lt;IoCContainer&amp;lt;object&amp;gt;&amp;gt; {
    if (IoCContainerSingleton.container) return this.container;

    // Create and initialize the container here...
    // Add your registrations and initialization logic

    return this.container;
  }
}

&amp;gt; Once this is done, it makes no sense to store the singleton in the container as you need an instance of the initialized container to be able to resolve the dependencies in it, and to access that you will anyway need to call this singleton's getInstance() method.

From this point, you will need a singletong implementation for each environment you'd like to run in. eg. test and production. But all the code is isolated in a dedicated file that has only this mission.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>Architecting a Pay-As-You-Go Service Library</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Tue, 03 Oct 2023 16:06:40 +0000</pubDate>
      <link>https://forem.com/zhamdi/architecting-pay-as-you-go-magic-usercredits-winning-formula-4ace</link>
      <guid>https://forem.com/zhamdi/architecting-pay-as-you-go-magic-usercredits-winning-formula-4ace</guid>
      <description>&lt;h1&gt;
  
  
  Architecting Pay-As-You-Go: UserCredits' Adaptable Design
&lt;/h1&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/zhamdi/a-pay-as-you-go-library-is-born-38jn"&gt;previous article&lt;/a&gt;, we introduced UserCredits, an open-source project designed to simplify the implementation of pay-as-you-go features in your applications. We explored the core concepts of UserCredits, including offers, orders, user credits, and token timetables. Now, let's dive deeper into the project's architecture, highlighting key design choices that make it both flexible and easy to extend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Need for Flexibility
&lt;/h2&gt;

&lt;p&gt;UserCredits aims to provide a flexible and technology-agnostic solution for managing pay-as-you-go features. To achieve this, we separate core business logic from specific database implementations. This separation allows us to support various databases without changing the fundamental logic of the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the DAO Factory
&lt;/h2&gt;

&lt;p&gt;At the heart of UserCredits' architecture is the DAO Factory (Data Access Object Factory). It abstracts the data access objects (DAOs) and their dependencies for various entities, such as offers, orders, token timetables, and user credits. By using this factory pattern, we decouple our business logic from the underlying database technology.&lt;/p&gt;

&lt;p&gt;Here's a closer look at how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IOfferDao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IOrderDao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ITokenTimetableDao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IUserCreditsDao&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../db/dao&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IDaoFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getOfferDao&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;IOfferDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;getOrderDao&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;IOrderDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;getTokenTimetableDao&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ITokenTimetableDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;getUserCreditsDao&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;IUserCreditsDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;IDaoFactory&lt;/code&gt; interface defines methods for creating DAO instances for different entities. Each DAO is responsible for interacting with a specific database collection. This separation of concerns makes it easy to switch between database implementations, whether it's MongoDB, PostgreSQL, or another system.&lt;/p&gt;

&lt;h2&gt;
  
  
  DAOs: Abstracting Database Operations
&lt;/h2&gt;

&lt;p&gt;DAOs (Data Access Objects) serve as the bridge between our business logic and the database. These DAOs provide methods for CRUD (Create, Read, Update, Delete) operations on database entities by default, but can be extended to more specific jobs.&lt;/p&gt;

&lt;p&gt;For example, here's a simplified version of the &lt;code&gt;IOfferDao&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IOfferDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;BaseDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createSubOffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;findSubOffers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IOffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other CRUD methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these DAO interfaces in place, we can implement concrete DAO classes for different database technologies. The key is that the business logic doesn't change; only the underlying DAO implementations do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexible Service Implementation
&lt;/h2&gt;

&lt;p&gt;UserCredits also provides a flexible &lt;code&gt;IService&lt;/code&gt; implementation, which serves as a facade for our pay-as-you-go features. This service abstracts complex operations, such as creating orders, executing payments, and checking remaining tokens. By keeping this logic in the service layer, we ensure consistency and simplify the testing process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IUserCredits&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;orderStatusChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refused&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;remainingTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IUserCredits&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;IService&lt;/code&gt; interface defines methods for performing high-level operations related to pay-as-you-go functionality. This separation of concerns allows us to write tests for our service without worrying about the underlying database implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Power of Inversion of Control (IoC)
&lt;/h2&gt;

&lt;p&gt;To bring everything together, UserCredits uses Inversion of Control (IoC) through the Awilix library. IoC allows us to manage dependencies and inject the appropriate DAOs into our service and other components.&lt;/p&gt;

&lt;p&gt;Here's how it works in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;asFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createContainer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;awilix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DaoFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./dao/DaoFactory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContainer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Register the DAO Factory as a transient value&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;daoFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;asFunction&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DaoFactory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dbFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Initialize and configure the database connections&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With IoC, we can easily swap out different implementations of the DAO Factory based on our database needs. This flexibility is essential for scaling and adapting UserCredits to various projects and technologies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;UserCredits' architecture is designed with flexibility and maintainability in mind. By separating core business logic, abstracting database operations with DAOs, and leveraging Inversion of Control, we've created a powerful framework for managing pay-as-you-go features. This architecture allows us to support different databases, write efficient tests, and adapt to specific project requirements seamlessly.&lt;/p&gt;

&lt;p&gt;In the following articles, we'll explore concrete payment processes, explain our tests, and start talking about UX use cases. Stay tuned for more insights into the evolution of this open-source project.&lt;/p&gt;




&lt;p&gt;This article provides a more comprehensive overview of UserCredits' architecture, focusing on the DAO Factory, IService implementation, and IoC. It aims to give readers a deeper understanding of how UserCredits achieves flexibility and adaptability in managing pay-as-you-go features.&lt;/p&gt;

</description>
      <category>payasyougo</category>
      <category>payment</category>
      <category>saas</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A pay as you go library for Node.js and Stripe on MongoDb</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Fri, 29 Sep 2023 09:12:17 +0000</pubDate>
      <link>https://forem.com/zhamdi/a-pay-as-you-go-library-is-born-38jn</link>
      <guid>https://forem.com/zhamdi/a-pay-as-you-go-library-is-born-38jn</guid>
      <description>&lt;h1&gt;
  
  
  UserCredits - Simplify Pay-As-You-Go Features for Your Project
&lt;/h1&gt;

&lt;p&gt;As an entrepreneur working on &lt;a href="http://cvlink.in" rel="noopener noreferrer"&gt;CvLink&lt;/a&gt;, I understand the challenges of adding payment and pay-as-you-go features to a project. It's a time-consuming process, and I wanted to build a library in public, documenting the reasons behind my decisions and giving readers the opportunity to provide feedback early in the process.&lt;/p&gt;

&lt;p&gt;The library is hosted on &lt;a href="https://github.com/ziedHamdi/UserCredits" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While there are existing solutions like &lt;a href="https://www.lemonsqueezy.com/" rel="noopener noreferrer"&gt;LemonSqueezy&lt;/a&gt; for implementing pay-as-you-go features, they can be overly complex for simple needs. You may just want to allow users to track their token consumption and pay for them, either before or after usage, without the complexity and cost of a full-fledged service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Typically Needed
&lt;/h2&gt;

&lt;p&gt;To better understand the scope, let's summarize the essential requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens&lt;/strong&gt;: An abstraction of money to decouple from real currency. For example, users can buy 10 tokens for $10 or 20 tokens for $18.&lt;/li&gt;
&lt;li&gt;Real-time tracking of token consumption or balance.&lt;/li&gt;
&lt;li&gt;A way for customers to pay for the tokens they've used or to purchase tokens upfront (depending on your billing model).&lt;/li&gt;
&lt;li&gt;Currency conversion to display token offers in multiple currencies.&lt;/li&gt;
&lt;li&gt;The ability to refund customers for unused tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Concepts
&lt;/h2&gt;

&lt;p&gt;Taking a straightforward approach, here are the core concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offers&lt;/strong&gt;: Represent how many tokens users get for their money. This includes scenarios like one-time offers, monthly subscriptions, yearly subscriptions, and discounts based on subscriptions. Offers can also override others with the same &lt;code&gt;overridingKey&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orders&lt;/strong&gt;: Represent customers' intentions to purchase an offer. We track their status until the payment is successful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Credits&lt;/strong&gt;: Provide information about a user's token balance and consumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Timetable&lt;/strong&gt;: A detailed log of each token consumption, including the date, time, and the service used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This seems to cover all the essentials for implementing a pay-as-you-go feature in a startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technologies
&lt;/h2&gt;

&lt;p&gt;Projects differ in their database and payment choices, so we expect the need for adapters on these axes. Therefore, it's crucial to keep concepts separate from implementations.&lt;/p&gt;

&lt;p&gt;Testing scenarios for buying, consuming, and checking credit balances should be dissociated from the specific technologies used, making them adaptable to different platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Here's how I've implemented these concepts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp7joqikjos7lnwm7j9w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp7joqikjos7lnwm7j9w.png" alt="Implementation Diagram" width="367" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Offer
&lt;/h3&gt;

&lt;p&gt;An offer specifies its type (e.g., one-time offer or subscription) and a repetition cycle. The &lt;code&gt;parentOffer&lt;/code&gt; property specifies relationships, such as discounts for subscribers. Offers with the same &lt;code&gt;overridingKey&lt;/code&gt; value override each other.&lt;/p&gt;

&lt;p&gt;Multiple sub-offers can share the same &lt;code&gt;overridingKey&lt;/code&gt;, representing different discount levels. For example, a free user could buy 10 AI credits for the regular price of $10, a &lt;strong&gt;"Starter"&lt;/strong&gt; user could get a 30% discount, and a &lt;strong&gt;"Early adopter"&lt;/strong&gt; user could have a 50% discount. These offers all have the same overriding key, e.g., &lt;code&gt;overridingKey="10_AI_Credits"&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The $10 offer at the root level (&lt;code&gt;parentOffer=null&lt;/code&gt;) for default visibility.&lt;/li&gt;
&lt;li&gt;The 30% off offer, a child of the "Starter" offer with a &lt;code&gt;weight=1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The 50% off offer, a child of the &lt;strong&gt;"Early adopter"&lt;/strong&gt; offer with a &lt;code&gt;weight=2&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This way, an &lt;strong&gt;"Early adopter"&lt;/strong&gt;, subscribed as a &lt;strong&gt;"Starter"&lt;/strong&gt;, gets better privileges by paying 50% less for AI tokens; while keeping the access rules associated to &lt;strong&gt;"Starter"&lt;/strong&gt; accounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IOffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yearly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expertise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;parentOffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tokenCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Order
&lt;/h3&gt;

&lt;p&gt;Orders are tied to users intending to purchase an offer. We track their status until payment success. The &lt;code&gt;status&lt;/code&gt; field is duplicated to simplify order reading, and the token count is duplicated for robustness.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refused&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nl"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refused&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tokenCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Token Timetable
&lt;/h3&gt;

&lt;p&gt;Token consumption and additions are logged in the token timetable for users to track their token activity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ITokenTimetable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  User Credits
&lt;/h3&gt;

&lt;p&gt;User credits summarize all transactions, including subscriptions and token balances. Subscriptions duplicate order status for efficient reading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ISubscription&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refused&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IUserCredits&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;ISubscription&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;)[];&lt;/span&gt;
  &lt;span class="nl"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Service
&lt;/h3&gt;

&lt;p&gt;The payment interface serves as a facade to manage these concepts. Customers can create orders, execute payments, and check their token balance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IPayment&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IUserCredits&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;orderStatusChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refused&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;remainingTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IUserCredits&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MongoDB was chosen for the initial implementation, but migrating to other databases will be straightforward.&lt;/li&gt;
&lt;li&gt;The payment part is yet to be implemented, with Stripe as the first choice, but flexibility for other implementations.&lt;/li&gt;
&lt;li&gt;Screens will be developed using SvelteKit, but the library is designed to be adaptable to React, Angular, and Vue as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I welcome early contributors to this project, so feel free to &lt;a href="https://twitter.com/zhamdi" rel="noopener noreferrer"&gt;contact me&lt;/a&gt; if you'd like to contribute or provide feedback.&lt;/p&gt;

&lt;p&gt;Thank you for your interest in the UserCredits library! 🚀&lt;/p&gt;

</description>
      <category>payment</category>
      <category>startup</category>
      <category>sveltekit</category>
      <category>saas</category>
    </item>
    <item>
      <title>500 open-source components for TailwindCSS</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Thu, 07 Sep 2023 07:55:31 +0000</pubDate>
      <link>https://forem.com/zhamdi/500-open-source-components-for-tailwindcss-4php</link>
      <guid>https://forem.com/zhamdi/500-open-source-components-for-tailwindcss-4php</guid>
      <description>&lt;p&gt;&lt;a href="https://tailwind-elements.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiy7q9a3ka0we5ywuk5o6.jpg" alt="Tailwind components" width="800" height="403"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
                       I'd like to share my latest discovery with you. &lt;br&gt;
       &lt;a href="https://tailwind-elements.com/" rel="noopener noreferrer"&gt;Tailwind Elements&lt;/a&gt; is currently, the most popular 3rd party UI kit for TailwindCSS with over 10k Github stars.        &lt;a href="https://github.com/mdbootstrap/Tailwind-Elements/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fgithub%2Fstars%2Fmdbootstrap%2Ftailwind-elements%3Fstyle%3Dsocial" alt="GitHub Repo stars" width="88" height="20"&gt;&lt;/a&gt;        It's a &lt;strong&gt;huge collection of stunning components&lt;/strong&gt; made with attention to the smallest detail.         Forms, cards, buttons, and hundreds of others.         All components have &lt;strong&gt;dark mode&lt;/strong&gt; and very intuitive &lt;strong&gt;theming options&lt;/strong&gt;.        The project is supported by an &lt;a href="https://github.com/mdbootstrap/Tailwind-Elements/discussions" rel="noopener noreferrer"&gt;engaged community on GitHub&lt;/a&gt;, I recommend you check it out and join one of the many discussions.&lt;br&gt;&lt;br&gt;
       You will find installation instructions &lt;a href="https://tailwind-elements.com/docs/getting-started/installation" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and you can track the progress of the project live         &lt;a href="https://tailwind-elements.com/docs/standard/getting-started/changelog/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
       The project was kickstarted by @MDBootstrap, a group of open-source developers behind &lt;a href="https://github.com/mdbootstrap/mdb-ui-kit" rel="noopener noreferrer"&gt;MDB UI Kit&lt;/a&gt; - a high-quality UI kit for Bootstrap, and also behind &lt;a href="https://mdbgo.com/" rel="noopener noreferrer"&gt;MDB GO&lt;/a&gt; - hosting and deployment platform.&lt;br&gt;&lt;br&gt;
       I highly recommend you to check it out!&lt;br&gt;&lt;br&gt;
       &lt;/p&gt;
&lt;div class="ltag__link"&gt;
  &lt;div class="ltag__link__content"&gt;
    &lt;div class="missing"&gt;
      &lt;h2&gt;Article No Longer Available&lt;/h2&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  With SvelteKit
&lt;/h2&gt;

&lt;p&gt;If you have a SvelteKit app and want to try out Tailwind Elements, here's a &lt;a href="https://tailwind-elements.com/docs/standard/integrations/svelte-integration/" rel="noopener noreferrer"&gt;great tutorial&lt;/a&gt; on how to set up your app.&lt;/p&gt;

</description>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>How I integrated preline to work in SvelteKit</title>
      <dc:creator>Zied Hamdi</dc:creator>
      <pubDate>Fri, 05 May 2023 10:36:56 +0000</pubDate>
      <link>https://forem.com/zhamdi/how-i-integrated-preline-to-work-in-sveltekit-16kj</link>
      <guid>https://forem.com/zhamdi/how-i-integrated-preline-to-work-in-sveltekit-16kj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftnti24msku4maqiabqfg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftnti24msku4maqiabqfg.jpg" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As I'm leaving &lt;a href="https://medium.com/@zhamdi" rel="noopener noreferrer"&gt;medium&lt;/a&gt; to come here, this is my first article on dev.to. I hope you'll like it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I decided to launch a resume builder with Sveltkit at &lt;a href="https://cvlink.in" rel="noopener noreferrer"&gt;CvLink&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After testing &lt;a href="https://flowbite-svelte.com/docs/components/card" rel="noopener noreferrer"&gt;Flowbite&lt;/a&gt;, I had a clear preference for the look and feel and component offer of &lt;a href="https://preline.co/examples/layouts-application.html" rel="noopener noreferrer"&gt;Preline&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But it was not easy to have the components react to click events. So I'm sharing what I did because I couldn't find any info on the web.&lt;/p&gt;

&lt;h1&gt;
  
  
  Steps
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I'm presenting the steps briefly as I suppose that's what you're waiting for: getting it done and walk away :-)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  install Preline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pnpm add preline&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add TailwindCss
&lt;/h3&gt;

&lt;p&gt;to do the equivalent of npx in pnpm:&lt;br&gt;
&lt;code&gt;npx svelte-add@latest tailwindcss&lt;br&gt;
pnpm i&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Install &lt;strong&gt;svelte-add&lt;/strong&gt; globally, then use it to add &lt;strong&gt;tailwindcss&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;pnpm install -g svelte-add&lt;br&gt;
svelte-add tailwindcss&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Configure tailwind
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;tailwind.config.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const config = {
content: [
"./src/**/*.{html,js,svelte,ts}",
"./node_modules/preline/dist/*.js",
],

    theme: {
        extend: {},
    },

    plugins: [
        require('preline/plugin')
    ],
    darkMode: 'class',
};

module.exports = config;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adapt Preline js files
&lt;/h3&gt;

&lt;p&gt;Instead of having the js load at &lt;code&gt;document.on('load')&lt;/code&gt; ex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.addEventListener('load', window.HSAccordion.init());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We make it load lazily when a component displays for its first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Files were copied from preline
&lt;/h3&gt;

&lt;p&gt;I copied &lt;a href="https://github.com/htmlstreamofficial/preline/blob/main/src/core/Component.js" rel="noopener noreferrer"&gt;./core/component.js&lt;/a&gt; to a local folder ./preline/component&lt;/p&gt;

&lt;p&gt;And copied &lt;a href="https://github.com/htmlstreamofficial/preline/blob/main/src/components/hs-accordion/index.js" rel="noopener noreferrer"&gt;HSAccordion&lt;/a&gt; to ./accordion/Accordion.js next to its Svelte component&lt;/p&gt;

&lt;h1&gt;
  
  
  Skipping js-loading when in SSR
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Accordion
&lt;/h2&gt;

&lt;p&gt;When index.js was loaded, it attached components' js to window like follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window.HSAccordion = new HSAccordion();
document.addEventListener('load', window.HSAccordion.init());

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I moved that init code to the AccordionItem component, checking i'm in the browser before initializing like follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    import Accordion from './Accordion';

    if(typeof window !== 'undefined') {
        console.log(  "initializing accordion" )
        window.HSAccordion = new Accordion();
        window.HSAccordion.init();
    }
    export let name;
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code for the accordion is pretty direct, then:&lt;br&gt;
&lt;strong&gt;AccordionItem.svelte&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    import Accordion from './Accordion';

    if(typeof window !== 'undefined') {
        console.log(  "initializing accordion" )
        window.HSAccordion = new Accordion();
        window.HSAccordion.init();
    }
    export let name;
&amp;lt;/script&amp;gt;

&amp;lt;li class='hs-accordion' id='account-accordion'&amp;gt;
    &amp;lt;a
        class='hs-accordion-toggle flex items-center gap-x-3.5 py-2 px-2.5 hs-accordion-active:text-blue-600 hs-accordion-active:hover:bg-transparent text-sm text-slate-700 rounded-md hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-slate-300 dark:hs-accordion-active:text-white'
        href='javascript:;'&amp;gt;
        &amp;lt;slot name='icon' /&amp;gt;
        &amp;lt;slot name='name'&amp;gt;{name}&amp;lt;/slot&amp;gt;

        &amp;lt;svg
            class='hs-accordion-active:block ml-auto hidden w-3 h-3 text-gray-600 group-hover:text-gray-500 dark:text-gray-400'
            width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'&amp;gt;
            &amp;lt;path d='M2 11L8.16086 5.31305C8.35239 5.13625 8.64761 5.13625 8.83914 5.31305L15 11'
                        stroke='currentColor' stroke-width='2' stroke-linecap='round'&amp;gt;&amp;lt;/path&amp;gt;
        &amp;lt;/svg&amp;gt;

        &amp;lt;svg
            class='hs-accordion-active:hidden ml-auto block w-3 h-3 text-gray-600 group-hover:text-gray-500 dark:text-gray-400'
            width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'&amp;gt;
            &amp;lt;path d='M2 5L8.16086 10.6869C8.35239 10.8637 8.64761 10.8637 8.83914 10.6869L15 5' stroke='currentColor'
                        stroke-width='2' stroke-linecap='round'&amp;gt;&amp;lt;/path&amp;gt;
        &amp;lt;/svg&amp;gt;
    &amp;lt;/a&amp;gt;

    &amp;lt;div id='account-accordion-child'
             class='hs-accordion-content w-full overflow-hidden transition-[height] duration-300 hidden'&amp;gt;
        &amp;lt;slot name='content' /&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/li&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created &lt;a href="https://github.com/ziedHamdi/preline-svelte.git" rel="noopener noreferrer"&gt;a repository&lt;/a&gt; where you can benefit of the work directly.&lt;/p&gt;

&lt;p&gt;Known bugs: Sometimes the js is not loaded, happens in dev mode, I still didn't check if it happens in prod.&lt;/p&gt;

</description>
      <category>sveltekit</category>
      <category>preline</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
