# Funkyton > Easily deploy and run complex open-source software with free starter templates and detailed guides. Simplifying setup for developers and beginners to save time and effort. Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### Welcome to Funkyton URL: https://funkyton.com/info/ Last updated: 2026-08-05T20:09:27.000Z This blog is your go-to resource for exploring open-source knowledge and tools. Inspired by the collaborative and empowering spirit of open source, Funkyton provides practical solutions to streamline your projects. From downloadable software and step-by-step guides to video tutorials, deployment templates, and 3D-printable designs, this platform is designed to help you get started quickly and efficiently. While some tools may require third-party services with associated costs, the resources here aim to simplify your journey into open source. ### Your support matters! I deeply appreciate everyone who engages with the projects shared on this blog. Whether you star a GitHub repository, leave a thoughtful comment on a blog post, or contribute to a YouTube discussion, your support means the world. Thank you for being part of this journey! ### Contact Have questions, feedback, or ideas for collaboration? Reach out via the email below, and I’ll personally respond to relevant inquiries. Email: info@funkyton.com ### Socials [FunkytonThis channel is all about making open-source alternatives simple and accessible for anyone who wants more control over their digital life. Here you will learn how to run your own services, explore self-hosting, and use open-source solutions without needing to be a professional developer. I create practical, step-by-step tutorials that break down the technical parts into plain language and focus on real setups you can actually run yourself. Over time, we have slowly handed over more and more of our digital lives to a handful of large tech companies. It happened through convenience, and “free” to use. At the same time, constant data collection became normal. Creative work is scraped to train AI models. Algorithms decide what we see. Most of us never consciously accepted this. Open-source solutions offer another path, where you decide where your data lives and what it is used for. If you care about privacy, affordability, and digital independence, you are in the right place.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)YouTube![](https://yt3.googleusercontent.com/1iTIFPzF3lg8-ZElyjepFffvW_dxCNNP_Zd554j2JfeoraGo6EAQzxRordthdMZdgOqLWIn4QQ=s900-c-k-c0x00ffffff-no-rj)](https://www.youtube.com/@funkyton?ref=funkyton.com) [rpuls - Overviewrpuls has 58 repositories available. Follow their code on GitHub.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)GitHub![](https://avatars.githubusercontent.com/u/14924038?v=4?s=400)](https://github.com/rpuls?ref=funkyton.com) [rpuls (@rpuls.bsky.social)https://funkyton.com![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Bluesky Socialrpuls![](https://cdn.bsky.app/img/avatar/plain/did:plc:zubj4vl5hfysd47p6ya6yzr5/bafkreib5aom6fp76wfd62ywfxktjb75bl724ohheh3s3yw4q6rulkz2oom)](https://bsky.app/profile/rpuls.bsky.social?ref=funkyton.com) [https://www.linkedin.com/in/rasmus-puls/](https://www.linkedin.com/in/rasmus-puls/?ref=funkyton.com) ### *In JavaScript, everything starts with a function. For me, everything starts with a typo 'funciton.' And that’s how Funkyton became the name of this blog about sharing functionality!* ### Tags URL: https://funkyton.com/tags/ Last updated: 2025-01-29T23:57:01.000Z Welcome to the tag overview! Here, you’ll find links to all the categories on Funkyton, each representing a unique area of focus. Whether it’s 3D printing, tutorials, open-source software, or smart home projects, these tags help you quickly dive into the topics that interest you most. [Deploy Templates for Open Source Projects | FunkytonDiscover one-click deploy templates for Medusa, Vendure, Payload, and more. Save time with easy setups and guides for open-source projects at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1614728263952-84ea256f9679?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHJvY2tldCUyMGxhdW5jaHxlbnwwfHx8fDE3MzgxOTI5NjR8MA&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/deploy-template/) [Medusa.js Tutorials, Deployment Guides & Templates | FunkytonExplore Medusa.js tutorials, pros and cons, and deployment guides. Launch scalable e-commerce projects with ease using our starter templates at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1592503254549-d83d24a4dfab?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fGVjb21tZXJjZXxlbnwwfHx8fDE3MzgwOTcwNTV8MA&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/medusa/) [Payload CMS Tutorials, Deploy Templates & Guides | FunkytonLearn Payload CMS with tutorials, deploy templates, and guides. Build dynamic websites and apps effortlessly using this modern headless CMS at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1565106430482-8f6e74349ca1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDZ8fHdlYiUyMGRldmVsb3BtZW50fGVufDB8fHx8MTczODE5MzE3Nnww&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/payload-cms/) [Open Source Software Tutorials & Deploy Templates | FunkytonDiscover open-source software tutorials, deploy templates, and guides. Simplify your setup and explore tools like Medusa, Payload CMS, and more at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1566837945700-30057527ade0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDE0fHxjb2RlfGVufDB8fHx8MTczODEzNzM3NHww&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/open-source-software/) [3D Printing Projects, Tutorials & Free Models | FunkytonDiscover 3D printing projects, free models, and tutorials. Learn tips and tricks to enhance your prints and explore innovative designs at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1638959492386-f9a68d55c374?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI0fHwzZCUyMHByaW50aW5nfGVufDB8fHx8MTczODE5MjkxMXww&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/3d-printing/) [Smart Home Projects, DIY Hacks & 3D-Printed Solutions | FunkytonDiscover smart home DIY projects, affordable hacks, and 3D-printed solutions. Upgrade your home with innovative ideas and guides at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1558002038-1055907df827?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fHNtYXJ0JTIwaG9tZXxlbnwwfHx8fDE3MzgxOTM1NDN8MA&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/smart-home/) [Step-by-Step Tech Tutorials: Open Source, 3D Printing & More |FunkytonDiscover tutorials on open-source tools, 3D printing, and deployment guides. Simplify projects with easy-to-follow steps and expert insights at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1586282023338-52aa50a846ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHR1dG9yaWFsfGVufDB8fHx8MTczODE5MzIxOHww&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/tutorial/) [Unity Tutorials, Scripting Guides & Game Development Tips | FunkytonDiscover Unity tutorials and scripting guides. Learn game development, animation, audio integration, and optimization techniques with Funkyton’s expert resources.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1556438064-2d7646166914?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGdhbWUlMjBkZXZlbG9wbWVudHxlbnwwfHx8fDE3MzgxOTM0MjJ8MA&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/unity/) ## Posts ### Goodbye Google Drive: My self-hosted Drive and Docs replacement URL: https://funkyton.com/goodbye-google-drive-my-self-hosted-drive-and-docs-replacement/ Last updated: 2026-08-11T20:35:00.000Z I have tried enough setups at this point to know the pattern. Nextcloud on my NAS, [Cloudreve on Railway](https://railway.com/deploy/cloudreve-1?ref=funkyton.com), Filebrowser as my daily driver, Syncthing. I have spent a lot of time with all of them. Some were unstable and frustrating to maintain. Others were solid, but missing key features. None of them managed to bring everything together into something that felt complete and reliable for daily use. About a year ago, I made the switch from Google Photos to Immich, and that changed how I look at this space. It proved that a self-hosted setup can actually replace a Big Tech service if the experience is good enough. Now it is time to do the same for Drive and Docs. I will share my exact setup later in the article so you can try it yourself in just a few minutes. Because this time, I am done testing. I am switching. ### Video ### Overview - [The problem with most Google Drive alternatives](#the-problem-with-most-google-drive-alternatives) - [What I tried and why it did not stick](#what-i-tried-and-why-it-did-not-stick) - [Nextcloud](#nextcloud) - [Cloudreve](#cloudreve) - [Filebrowser](#filebrowser) - [Honorable mention: Syncthing](#honorable-mention-syncthing) - [Why I landed on Seafile + ONLYOFFICE](#why-i-landed-on-seafile-onlyoffice) - [Seafile](#seafile) - [ONLYOFFICE](#onlyoffice) - [AI support as a bonus](#ai-support-as-a-bonus) - [The trade-offs](#the-trade-offs) - [Try this setup yourself in minutes, for free](#try-this-setup-yourself-in-minutes-for-free) - [Deploy](#deploy) - [This is just the beginning of something bigger](#this-is-just-the-beginning-of-something-bigger) - [Final thoughts](#final-thoughts) ## The problem with most Google Drive alternatives Replacing Google Drive, Dropbox, or OneDrive is not just about storing files. That part is easy. What actually matters is getting three things to work together in a way that feels seamless: - File storage - Sync across devices - Browser-based document editing Most self-hosted cloud storage tools get one or two of these right. Very few get all three working together in a way that feels like a single product. That is where most setups break down. ## What I tried and why it did not stick ### Nextcloud Nextcloud is the obvious choice when looking for a self-hosted cloud or Google Drive alternative. It is popular, widely recommended, and tries to cover everything. For me, that was part of the problem. It feels more like a full ecosystem than a focused tool, and one of the reasons I am moving away from Google is exactly that kind of all-in-one platform. I do not want another system trying to do everything at once. In practice, I ran into a few recurring issues: - The UI feels overloaded - Too many settings exposed to the user - Features I actually needed were not reliable The breaking point was iOS photo backup silently stopping. No warning, no clear error, and I never managed to get it working again. I also ran into version mismatches between iOS and the server, and repeated instability with the Collabora integration. Containers restarting, services crashing, things behaving unpredictably. To be fair, this is most likely on me. I probably did not set it up in the best possible way. Maybe my hardware or OS was not a great fit. I spent an unusual amount of time trying to make it stable, far more than I would expect for something that should be a core part of my setup. That alone is a red flag for me going forward. I do not want my cloud storage to be a debugging project. ### Cloudreve [![Screenshot of Cloudreve UI from the official GitHub repository](https://raw.githubusercontent.com/cloudreve/docs/master/images/homepage.png)](https://railway.com/deploy/cloudreve-1?ref=funkyton.com) Screenshot of Cloudreve UI from the official GitHub repository Cloudreve was a pleasant surprise in some ways. The UI is easily one of the nicest I have seen. It feels modern and polished, closer to a commercial product than most self-hosted tools. But it did not fully solve the problem. - It leans into integrations I do not need - No proper office suite integration - Some features feel out of place for a personal setup It got close, but "almost" is not enough when you are trying to replace something you rely on daily. If you are curious about Cloudreve, [visit the deploy template page](https://railway.com/deploy/cloudreve-1?ref=funkyton.com). ### Filebrowser ![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/filebrowser-tested-alternative-png.png?_a=BAMAJafi0) [Filebrowser](https://filebrowser.org/?ref=funkyton.com) is what I am currently using every day, and I am **not** dropping it. It is excellent at what it is built for: - Browsing files - Uploading and downloading - Creating share links (with expiration and password protection) - Managing a large filesystem on a NAS But that is also where its scope ends. There is no sync layer, no collaboration, no document editing. It solves a different problem, just not the one I am targeting here. It is a great tool, but it is not a real alternative to Google Drive, Dropbox, OneDrive, or iCloud. ### Honorable mention: Syncthing One tool that absolutely deserves a mention here is Syncthing. I have been using it to synchronize saved game data across multiple devices, for example between my desktop and handheld devices, and it has been rock solid. No manual copying, no thinking about versions, it just makes sure the latest save is always available wherever I pick up the game. That kind of reliability is rare. For pure file synchronization, Syncthing is honestly one of the best open-source tools out there. It is fast, peer-to-peer, and does not rely on a central server in the same way most cloud solutions do. But it solves a very specific problem. There is no real user interface for browsing files like a cloud drive, no sharing layer, and no integration with document editing. So while it is excellent for keeping folders in sync, it is not a replacement for something like Google Drive and Docs. Still, for what it does, it does it extremely well, and it is definitely a tool worth having in your stack. ## Why I landed on Seafile + ONLYOFFICE This is the first setup that actually feels complete. Not because it has the most features, but because it focuses on the right ones and connects them properly. ### Seafile ![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/seafile-a-google-drive-alternative-open-source-and-self-hosted-png.png?_a=BAMAPqkS0) [Seafile](https://www.seafile.com/?ref=funkyton.com) handles the file side of things, and it does it really well as a free to use, self-hosted cloud storage solution. It gives you: - Fast and reliable sync - Clean file organization - Proper sharing, including password-protected links - A stable experience without random issues What I like most is that it stays in its lane. It does not try to become an entire ecosystem. It is a focused tool, and that shows. ### ONLYOFFICE ![Real time collaboration between users in ONLYOFFICE](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/onlyoffice-in-seafile-real-time-collaboration-chat-comments-self-hosted-png.png?_a=BAMAJafi0) Real time collaboration between users in ONLYOFFICE [ONLYOFFICE](https://www.onlyoffice.com/?ref=funkyton.com) is what turns this from storage into a real self-hosted office suite workflow. Most self-hosted document editors feel slightly off. Either the UI is clunky, or collaboration does not work properly, or file compatibility becomes a problem. ONLYOFFICE avoids all of that. - Real-time collaboration works - The UI feels modern and familiar - Supports Word, Excel, and PowerPoint formats - Comments and track changes behave as expected The integration is the key part. Files live in Seafile, and when you open them, they launch directly in ONLYOFFICE in the browser. No downloads. No friction. No extra steps. ### AI support as a bonus This was not something I was actively looking for, but it ended up being a really nice addition. ONLYOFFICE also supports AI integrations if you want them. You can connect your own provider using your own API key and use LLM features directly in the editor for things like translation, grammar, and writing assistance. The important part is that it is not tied to a specific provider. You can choose whatever AI service you trust, or if you have the hardware, even run your own. That is very different from something like Google Docs, where you are locked into their AI and pricing, and where it is not always clear how your data is used. If you want AI features with more control over which provider handles your documents, this is a much more privacy-focused, self-hosted approach. ## The trade-offs This setup is not perfect, and it is worth calling that out clearly: - Seafile UI is good, but not the most modern - Cloudreve still wins on visual polish - It is SSR-based, which is not my personal preference, though others may see that as a plus - I did run into some minor cache-related quirks But none of these issues blocked me from actually using it daily. That is the key difference. ## Try this setup yourself in minutes, for free If you want to try a self-hosted alternative to Google Drive, Dropbox, or OneDrive with browser-based document editing, I have made it very easy to do so. I put together a Railway template that sets everything up for you. No manual configuration, no wiring services together, no figuring out how to make Seafile talk to ONLYOFFICE. ![Seafile and ONLYOFFICE on Railway, project canvas](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/seafile-and-onlyoffice-self-hosted-on-railway-one-click-deploy-template-project-canvas-png.png?_a=BAMAJafi0) Seafile and ONLYOFFICE on Railway, project canvas **What you need** - A Railway account (signing up with GitHub is recommended) That is enough to deploy and try it. New Railway users can test this stack on the free trial, which currently includes enough credits and supports up to 5 services per project. If you decide to keep it running long-term, you will need at least the hobby plan, at 5$/m. ### Deploy [ ![Deploy on Railway](https://railway.app/button.svg) ](https://railway.com/deploy/seafile-onlyoffice?referralCode=-Yg50p&ref=funkyton.com) The template deploys the full stack with the integration already configured, so you can go straight to uploading files, opening documents, and testing the workflow in the browser. Give it a few minutes to finish deploying, upload a couple of files, open them in the editor, and you will quickly get a feel for whether this setup works for you. ![Locate your Seafile login credentials](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/seafile-and-onlyoffice-self-hosted-on-railway-one-click-deploy-template-locate-login-credentials-png.png?_a=BAMAJafi0) Locate your Seafile login credentials ![Locate your public URL](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/seafile-and-onlyoffice-self-hosted-on-railway-one-click-deploy-template-locate-public-url-png.png?_a=BAMAJafi0) Locate your public URL That's it. Go to your public URL as shown in the screen above, log in with your autogenerated credentials, and you're in! You can create a proper personal user if you want to, from the settings menu. ## This is just the beginning of something bigger This setup is part of a bigger project I am working on: My Own Suite. The goal is to build a full alternative to the typical Big Tech stack, but in a way that is actually approachable. Not just for homelab setups, but for normal users who just want their own services without the complexity. Seafile and ONLYOFFICE cover files and documents. Other pieces are being added over time to complete the picture. If you are interested, you can read more here: [https://myownsuite.org/](https://myownsuite.org/?ref=funkyton.com) --- ## Final thoughts After two years of trying different setups, this is the first time I feel confident enough to actually switch. Not just test. Not just experiment. Switch. That is a big difference. This might finally be the point where I can say goodbye to Google Drive and similar cloud services for good. If you have any questions or thoughts, feel free to leave a comment below. Thanks for reading! ### Deploy Vendure to a single VPS with Dokploy URL: https://funkyton.com/deploy-vendure-to-a-single-vps-with-dokploy/ Last updated: 2026-03-19T15:29:20.000Z Running Vendure in production can feel overwhelming at first, especially when infrastructure is involved. In this article, we'll walk through how to deploy the headless e-commerce framework **Vendure** to a single VPS using **Dokploy**, keeping things as simple and practical as possible. The focus will be on infrastructure: what components Vendure needs, how they fit together, and how to deploy them in a way that’s suitable for production. If you'd rather skip the explanations and just follow the steps, you can jump straight to the [Github repository](https://github.com/Pinelab-studio/vendure-dokploy-starter?ref=funkyton.com). ### Video guide # Prerequisites - This guide assumes you already have a VPS running somewhere with: - At least 4 GB memory. - Ubuntu or Debian installed. See [Dokploy's documentation](https://docs.dokploy.com/docs/core/installation?ref=funkyton.com) for information on the supported Linux distros. - Make sure your server accepts traffic on ports the following ports: - 3000 (Dokploy) - 80 and 443 (HTTP and HTTPS) - 5432 (Postgres) - 6379 (Redis) # Install Dokploy First we will be installing Dokploy. Dokploy is a platform that allows you to manage Docker containers via a UI on your VPS - First, SSH in to your machine, and [install Dokploy](https://docs.dokploy.com/docs/core/installation?ref=funkyton.com). Make sure you also [complete the setup](https://docs.dokploy.com/docs/core/installation?ref=funkyton.com#completing-the-setup). - By default, Docker will not prune log files, so you need to set up log file rotation to prevent ever growing log files. Follow the steps [described here](https://github.com/Pinelab-studio/vendure-dokploy-starter?tab=readme-ov-file&ref=funkyton.com#dokploy) to do so. - Go to :3000> and log in to your Dokploy instance and enable 2FA: `Profile` \> `Enable 2fa` in the top right corner. Now that Dokploy is installed, we can go ahead and create a project named `Vendure`. This is where our services will be created: a main API instance, a Vendure worker, PostgreSQL and Redis. # Set up Postgres Postgres will be the database where Vendure will store its data, products, collections, orders, etc. Follow the steps [described here](https://github.com/Pinelab-studio/vendure-dokploy-starter?tab=readme-ov-file&ref=funkyton.com#postgresql) to create a PostgreSQL database with Dokploy and expose it to the internet. You should restrict access to port 5432 to only allow your local IP address! This is usually done on the VPS provider level, but can also be done with a tool like `ufw` Check if you can log in to the database from your local machine with `pgsql` or a tool like [Beekeeper](https://www.beekeeperstudio.io/?ref=funkyton.com). # Set up Redis Vendure offloads intensive tasks to a separate worker service and uses BullMQ to do so. BullMQ is a library that uses Redis as a job queue. Vendure has some [nice documentation](https://docs.vendure.io/guides/developer-guide/worker-job-queue/?ref=funkyton.com) on the concept of its worker and job queue. Setting up Redis is, just like Postgres, pretty straightforward using the Dokploy UI. The detailed steps are [described here](https://docs.vendure.io/guides/developer-guide/worker-job-queue/?ref=funkyton.com). The same here as for Postgres: make sure you only allow access to port 6379 from your local IP address. # Setup Vendure Now that we have Postgres and Redis set up, we can set up Vendure. First we will try to run Vendure locally to verify if Postgres and Redis are set up correctly. - Fork and clone [this repository](https://github.com/Pinelab-studio/vendure-dokploy-starter?ref=funkyton.com) - Create an `.env` file in `./vendure/.env` with the variables [defined here](https://github.com/Pinelab-studio/vendure-dokploy-starter?tab=readme-ov-file&ref=funkyton.com#vendure-api). - Run `npm ci` to install Vendure's dependencies. - Run `npx vite build` to build Vendure's dashboard. - We can now try to run the Vendure server with `npm run dev:server` - Go to [http://localhost:3000/dashboard](http://localhost:3000/dashboard?ref=funkyton.com) to check if your server has started successfully. If all is well, you should be able to log in. This means Vendure is configured correctly, and it can reach our database and Redis. ## Build and push to Docker Hub. We will use GitHub Actions to package our Vendure project into a Docker image and push it to Docker Hub. This means you will have to create a Docker Hub account. But don't worry, a free account is all we need. 💡 If you just want to see Vendure in action, without creating a Vendure project and building a Docker image, you can use our prebuilt image `martijnfrompinelab/dokploy-vendure-demo:latest` in the next step. Follow the steps [here](https://github.com/Pinelab-studio/vendure-dokploy-starter?tab=readme-ov-file&ref=funkyton.com#build-docker-image) to set the correct environment variables in your GitHub secrets and enable GitHub Actions. Now, when you push a change to your main branch, [this GitHub action](https://github.com/Pinelab-studio/vendure-dokploy-starter/blob/main/.github/workflows/build-and-deploy.yml?ref=funkyton.com) will build the Docker image and push it to Docker Hub. As a final smoke test, check your Docker Hub account to see if the image exists, and note down the name. ## Create the Vendure service in Dokploy The final step for the Vendure API instance is to make Dokploy deploy the Docker image to our VPS. We need to create a service and [set the correct settings in Dokploy for the service.](https://github.com/Pinelab-studio/vendure-dokploy-starter?tab=readme-ov-file&ref=funkyton.com#deploy-the-image) When you click `Deploy` in the `General` tab, Dokploy will pull the image from Docker Hub and run it on your VPS! Go to `vendure.yourcustomdomain.io/dashboard` to verify that Vendure is running. 💡 ****About domains** It is perfectly fine to use subdomains or even sub-subdomains. For example, we used `dokploy.test.pinelab.app`, `vendure.test.pinelab.app` and `vendure-worker.test.pinelab.app`. You create A records for each of these subdomains and set the IP of your VPS as value for those records. ## Vendure Worker The Vendure Worker service will use the same Docker image, so there's no need to repeat those steps. Configuring the worker service in Dokploy is similar to the main instance, [with some minor differences](https://github.com/Pinelab-studio/vendure-dokploy-starter?tab=readme-ov-file&ref=funkyton.com#vendure-worker). After your worker has been deployed, your Vendure setup is complete! This is all you need to get Vendure up and running on your VPS. For a truly production-ready environment, continue reading and apply the following final adjustments. # Load test We have tested our 4 GB 2 vCPU instance to be capable of handling about **240 orders per minute** before the p95 response time exceeds 500 ms. You can read more on the test setup and how to load test your own environment with the provided script [here](https://github.com/Pinelab-studio/vendure-dokploy-starter/tree/main/load-test?ref=funkyton.com). # Resource allocation explained Setting resource limits per service prevents a single service from using all available CPU or, worse, causing an Out Of Memory error on your VPS! Luckily, Dokploy allow us to set CPU and memory limits per service. An important rule of thumb is that **the sum of all assigned memory in Dokploy services should never exceed the total amount of memory of your server**. You can safely oversubscribe CPU (and should) because it is **time-shared**. If multiple containers demand more CPU than the VPS has, the kernel does not kill them; it simply **throttles** them. Setting some CPU limits for some services is still a good idea, to prevent one service from consuming all available CPU for a longer time. On our 2 vCPU and 4 GB memory instance, we will do the following: | Service | Memory Limit | CPU Limit | | ---------------- | ------------ | --------- | | Vendure main API | 1 GB | None | | Vendure worker | 0.5 GB | 1 CPU | | Postgres | 1 GB | None | | Redis | 0.5 GB | 1 CPU | This uses 3 GB of memory in total, leaving 1 GB for the OS, Dokploy and Docker itself. These resource limits can be set in the `Advanced` tab of each service and are set in bytes and CPU nanoseconds: - 0.5 CPU would be defined as `500000000` - 500 MB memory limit would be defined as `524288000` # Scaling up Once your server starts to get more traffic, you might want to increase the memory and CPU of your VPS. Node.js will benefit from more than one CPU for things like crypto and file system I/O, **but because Node.js itself is single-threaded, you will eventually need to create multiple Vendure main instances in Dokploy to handle more traffic**. You can use the [replica setting](https://docs.dokploy.com/docs/core/applications/advanced?ref=funkyton.com#cluster-settings) to increase the number of replicas of the Vendure main API service. This will make Dokploy spin up multiple replicas of the same Docker image, with the same config. Dokploy's Traefik will split traffic evenly between the replicas. You can do the same for the Vendure worker service if needed. # Backups Dokploy allows you to easily create backups to S3-compatible destinations. Dokploy supports both [automatically creating backups](https://docs.dokploy.com/docs/core/databases/backups?ref=funkyton.com) of Postgres and [volume backups](https://docs.dokploy.com/docs/core/volume-backups?ref=funkyton.com) for Vendure's assets. # Security - Make sure to follow [Dokploy's security recommendations](https://docs.dokploy.com/docs/core/remote-servers/security?ref=funkyton.com). - Only allow access to ports 5432 and 6379 from specific IP addresses. These ports are only exposed so that you can run your local Vendure against the live Postgres and Redis instances. - You should set up resource alerts for the VPS as a whole with your provider. For example: - Notify me when CPU is above 80% - Notify me when Memory usage is above 80% - Use a tool like Uptime Kuma to periodically check both Vendure's main and worker `/health` endpoints. --- That's it! Feel free to reach out to me if you have any questions. Happy self-hosting! ### MercurJS: Host Your Own Multi-Vendor Marketplace URL: https://funkyton.com/mercurjs-deploy-tutorial/ Last updated: 2026-07-28T20:29:46.000Z Since I have already gotten my hands dirty with Medusa and Vendure, two incredible open-source e-commerce ecosystems, it would almost be weird not to also cover Mercur. In this article, I’ll lay out all the strengths and shortcomings that Mercur brings to the table. And, as always, I've put a lot of time into creating a one-click deploy template so that you can try the entire MercurJS multi-vendor marketplace stack for yourself in just a matter of minutes, with minimal setup required. ### Video ### Overview - [Mercur Explained in 1 Minute](#mercur-explained-in-1-minute) - [The Marketplace Owner Experience](#the-marketplace-owner-experience) - [The Vendor Experience](#the-vendor-experience) - [The Customer Experience](#the-customer-experience) - [The Developer Experience](#the-developer-experience) - [Final thoughts](#final-thoughts) - [Deploy Your Own MercurJS Marketplace with 1 click](#deploy-your-own-mercurjs-marketplace-with-1-click) - [Prerequisite](#prerequisite) - [Deploy](#deploy) - [Log in to your Admin Dashboard](#log-in-to-your-admin-dashboard) - [Next Steps: Customization and Local Development](#next-steps-customization-and-local-development) - [Eject Template to Make Customizations](#eject-template-to-make-customizations) - [Run the Project Locally](#run-the-project-locally) - [Stripe and Resend Integration](#stripe-and-resend-integration) - [Got Questions or Need Help?](#got-questions-or-need-help) ## Mercur Explained in 1 Minute ![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mercurjs-dahsboard-interfaces-stacked-graphic-png.png?_a=BAMAJafi0) [Mercur](https://www.mercurjs.com/?ref=funkyton.com) is a platform for building custom marketplaces using modern technologies, with complete feature coverage and full code ownership. It consists of five core solutions: Mercur Core, the Admin Panel, the Vendor Panel, [Mercur Connect](https://www.mercurjs.com/connect?ref=funkyton.com), and Mercur Storefronts, giving teams full flexibility over marketplace operations and customer experience. If you already know what Mercur is and just want to [jump straight to the quick deploy tutorial](#deploy-your-own-mercurjs-marketplace-with-1-click), feel free to skip ahead. But if you'd like a clear, no-nonsense overview of what Mercur actually is, who it's for, and how it works for vendors, customers, and platform owners, this section will give you exactly that. I'll lay out what each type of user can expect from Mercur, so you'll quickly understand its value and fit for your needs. ### The Marketplace Owner Experience As the owner or administrator of a Mercur marketplace, you're responsible for hosting and managing the platform. Your main job is to ensure reliable uptime, fast load speeds, and a professional setup that helps attract vendors and customers. You set the marketplace rules, approve or reject vendor sign-up requests, and manage everything through an intuitive super admin dashboard. You also have the freedom to shape the branding and concept of your marketplace to stand out. The stack comes with a polished default storefront themed as a clothing/fashion marketplace, but if you want to go beyond that, you’ll need development skills or access to someone who can code. Mercur gives you full control, but complex customization isn't something anyone can build quickly or without technical know-how. If you're comfortable with coding or can work with a developer, the possibilities are nearly limitless. If not, you'll be limited to the platform’s out-of-the-box features. ### The Vendor Experience Mercur offers an ultra-low entry barrier for vendors. To start selling, all you need to do is sign up with your company or store name, email, and password, then wait for approval from the marketplace administrator, and then set up or import your products. This process is much simpler than deploying your own Medusa store. No technical skills needed, and you don't have to worry about hosting or domain setup. Anyone can start selling within minutes. Of course, this convenience comes with trade-offs. Outsourcing marketplace management and hosting means ceding some control to the marketplace owner. You'll need to accept that they set the platform rules and have the final say in most decisions. For those valuing independence and full control, this is a significant consideration that shouldn't be overlooked. ### The Customer Experience Depending on how the marketplace owner decides to brand the platform, customers might or might not notice that products come from multiple individual vendors. Each customer might see a different catalog of products depending on their geographical region, since not all vendors ship everywhere. But otherwise, the shopping experience feels like any other webshop operating from a single warehouse. However, if the marketplace owner wants to emphasize each vendor's uniqueness (think Etsy), it's entirely possible to brand the platform that way too. The bottom line: Customers use one web interface to buy from multiple vendors, with the same checkout flow and payment options no matter what they purchase. ### The Developer Experience For anyone who enjoys working with a modern tech stack (Node.js and TypeScript), the developer experience is about as good as it gets. Beyond that, MercurJS is built on top of MedusaJS (2.x), so if you've worked with Medusa before, you’ll feel instantly at home. To put that in perspective, my [deployment automation library](https://www.npmjs.com/package/medusajs-launch-utils?ref=funkyton.com), which I originally created for MedusaJS projects, worked with Mercur right out of the box. MercurJS retains Medusa’s developer‑first philosophy: modular, extensible, and designed to give developers full control over how features are built and integrated. Whether you're adding custom logic, connecting external APIs, or extending core functionality, MercurJS gives you the freedom to build things your way. For developers that aren't familiar with Medusa, there's definitely a bit of a learning curve to get started, however, the documentation has been going through a major overhaul recently, and the built‑in [AI assistant on the Medusa docs](https://docs.medusajs.com/?ref=funkyton.com) is genuinely helpful. I've used it countless times while working with Mercur, both to clarify concepts and to debug tricky implementation details. *This is an article about MercurJS, but hey... Shout out to the Medusa team! 👏* ### Final thoughts It's pretty remarkable that you can build a fully functional multi‑vendor marketplace on a free, open‑source foundation. Platforms like this are usually built for serious operations, multiple vendors, real transaction volumes, and complex infrastructure, not side projects or small merch stores. The fact that MercurJS makes that level of capability accessible to independent developers is impressive. That said, Mercur isn't just another webshop starter kit. It's designed for more ambitious projects than I usually approach; the kind of marketplace where scalability, vendor management, and automation actually matter. For small or traditional webshops, Medusa (or Vendure) might still be the better fit. But for developers or teams building marketplaces, MercurJS delivers an incredible head start. From a technical standpoint, my experience with MercurJS has been positive, with no serious red flags. I did encounter a few hiccups during CLI setup, the kind you expect with projects of this complexity. However, it was running locally within half an hour and felt stable. I haven't performed a large‑scale stress/load test because of how complex and time-consuming that is to do, but given the underlying Medusa architecture, scaling should be a solved problem, simply by adding more worker instances when needed. If your project calls for a multi‑vendor marketplace, MercurJS is absolutely worth exploring. It's rare to see this much capability and polish available completely open‑source, especially for something this advanced. ## Deploy Your Own MercurJS Marketplace with 1 click As usual, I've put some time and effort into streamlining and automating the deployment process, so you don't have to fiddle with any configuration or setup. In just a few minutes, you can have a fully working MercurJS marketplace deployed in the cloud and see it in action for yourself. ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mercur-service-overview-deploy-on-railway--updated-july-2026-png.png?_a=BAMAPqkS0) Railway project canvas The entire stack, as seen in the diagram above, can be launched with just one click. ### Prerequisite - [GitHub](https://github.com/?ref=funkyton.com) account - Railway account (connected with GitHub) - Railway Hobby plan ($5/month) This is enough for you to deploy the template, it will build and provide you with all the individual dashboards and the marketplace storefront, BUT, the Mercur platform also really depends on a few other, optional, but recommended pieces to get the most out of it. If you are planning on accepting payments, sending automated emails, and offering a product search engine. A few API keys can be added during the deploy process. The following **optional dependencies** are: - [Algolia search engine](https://www.algolia.com/?ref=funkyton.com) API key (Recommended for testing) - [Stripe payment](https://stripe.com/?ref=funkyton.com) API key (can be added later, not needed for testing) - [Resend emailing](https://resend.com/?ref=funkyton.com) API key (can be added later, only needed for email sending) Protip: Ignore Stripe and Resend right now, but make sure to get your Algolia API key ready. It's really easy to sign up, you can use your existing GitHub account to. That's what I did. If you happen to already have Stripe and Resend API keys, feel free to add them during the deployment, but if not, wait until you actually need to start sending emails and accepting payments. Links to [Stripe and Resend signup and configuration guides](#stripe-and-resend-integration) can be found at the bottom of this article. For now, just sign up with Algolia, and go the the API key dashboard. ![How to find Algolia API keys and Application ID](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/algolia-dashboard-api-keys-png.png?_a=BAMAJafi0) How to find Algolia API keys and Application ID Keep this tab open, you will need it in the next step. ### Deploy You are now ready to start the template deployment. Click the button below to go to the template overview. [ ![Deploy on Railway](https://railway.app/button.svg) MercurJs v1.5.3 Updated on 28th July 2026 ](https://railway.com/deploy/mercurjs?referralCode=-Yg50p&ref=funkyton.com) ![MercurJS service overview on Railway](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mercur-service-overview-deploy-on-railway-deploy-button-png.png?_a=BAMAJafi0) MercurJS service overview on Railway Once you click the deploy button on the template overview, you will be taken to the initial setup page. As mentioned above, the template will build and deploy without doing anything in this step, but if you want to full experience, add your Algolia details. Expand the Marketplace container, and add your Algolia Application ID in the `NEXT_PUBLIC_ALGOLIA_ID` field, and add **Search** API Key in the `NEXT_PUBLIC_ALGOLIA_SEARCH_KEY` field. Click Save Config. Then, expand the backend, and add your Algolia Application ID in the `ALGOLIA_APP_ID` field, and add **Write** API Key in the `ALGOLIA_API_KEY` field. Click Save Config. ![MercurJS Storefront and Backend setup, initial deploy on Railway ](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/configure-api-keys-for-mercur-png.png?_a=BAMAJafi0) MercurJS Storefront and Backend setup, initial deploy on Railway This is where you provide all the API keys prior to deploy, but as I mentioned earlier, it's easy to add the Stripe and Resend API keys later. Once you have added your Algolia keys and application ID and saved, click the "Deploy Now" button. This will start the automated deploy process, which will take at least 10 minutes, longer is also normal. I don't want to go too much into details about how I automated the deployment process right now, but I do want to mention that this was made possible with my [medusajs-launch-utils](https://www.npmjs.com/package/medusajs-launch-utils?ref=funkyton.com) library, which is used in all of my other MedusaJS templates. It provides some helper functions, such as cross-service API key sharing and build queueing, which make deployment on Railway easy. ### Log in to your Admin Dashboard Once you have all green checkmarks, you are ready to log in to your admin dashboard. To do that, you first need to locate your admin user credentials, which can be found on the Backend service, in the **Variables** section. ![Find MercurJS super admin credientials, in backend variables](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mercur-locate-your-super-admin-login-credentials-png.png?_a=BAMAJafi0) Find your MercurJS super admin credentials in the backend variables section Next, open up the login page. You'll find the URL on the **Deployments** section on the Admin-dashbaord service. ![Find URL to MercusJS super admin dashboard](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mercurjs-on-railway-locate-url-to-admin-dashboard-png.png?_a=BAMAJafi0) Find the URL to your MercurJS super admin dashboard Clicking this link will take you to the login page. Paste your credentials and log in. ![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mercurjs-admin-dashboard-login-page-png.png?_a=BAMAJafi0) MercurJS super admin login page That's it! You are done with the cloud setup 🥳 ## Next Steps: Customization and Local Development Once you've tested your Mercur marketplace in the cloud and confirmed everything works as expected, it's time to take ownership of the codebase. The next steps walk you through ejecting the project from the deployment template, running it locally, and preparing it for real‑world use, including setting up payments and automated emails. ### Eject Template to Make Customizations When you deploy the Railway template, all app instances are initially linked to my public GitHub repository, except for the storage Bucket and the proxy function. To make changes, you'll need your own version of the code. That's exactly what the Eject button does, it tells Railway to create a personal GitHub fork of the project under your account. You can find it under **Source Repo** in the **Settings** section on the **Backend** service. ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/eject-mercurjs-template-on-railway-to-get-personal-fork-repository-png.png?_a=BAMAJafi0) How to find the Eject button in Railway Once ejected, your deployment pipeline will use that fork as its source repository, giving you full control to edit, push updates, and customize as you like. *(This is also why the prerequisite step recommends signing up to Railway using your GitHub account, it allows Railway to create the fork on your behalf.)* ### Run the Project Locally Once you’ve ejected the template and Railway has created your personal GitHub fork, you can clone that repository to your local machine. This gives you full access to the code for development, debugging, and customization. For detailed setup steps, including environment variables, dependencies, and commands, check the latest instructions in the [GitHub README](https://github.com/rpuls/mercurjs-for-railway-boilerplate?ref=funkyton.com). It's kept up to date with any changes or requirements. ### Stripe and Resend Integration If you've made it this far and want to take your MercurJS project beyond testing, adding support for payments and automated emails is the next logical step. MercurJS uses **Stripe** for payments and **Resend** for transactional emails, both optional during early setup but essential for any real, production‑ready marketplace. I've previously written guides on how to integrate these services with **MedusaJS 2.0**, and while they're not written specifically for Mercur, the setup process is nearly identical. They should still serve as a helpful reference when you’re ready to configure these integrations: ### **Stripe** [How to add Stripe payment module to medusajs 2.0Easily integrate Stripe payments into your MedusaJS 2.0 setup with this guide. Includes links to documentation, configuration examples, and video instructions. Set up webhooks, environment variables, and payment events for a seamless checkout experience in your Medusa webshop!![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/blog-cover-medusa-2.0-stripe-integration.jpg)](https://funkyton.com/add-stripe-payment-module-to-medusajs-2-0/) [Stripe Payments - MercurJS documentation![](https://static.ghost.org/v5.0.0/images/link-icon.svg)MercurJS documentation![](https://mercur.mintlify.app/mintlify-assets/_next/image?url=%2F_mintlify%2Fapi%2Fog%3Fdivision%3DIntegrations%26title%3DStripe%2BPayments%26logoLight%3Dhttps%253A%252F%252Fmintcdn.com%252Fmercur%252FwRG-LXpRtE5ezy37%252Flogo_dark.svg%253Ffit%253Dmax%2526auto%253Dformat%2526n%253DwRG-LXpRtE5ezy37%2526q%253D85%2526s%253D8616f2bf1a219d2cd10a9f88f03b253d%26logoDark%3Dhttps%253A%252F%252Fmintcdn.com%252Fmercur%252FwRG-LXpRtE5ezy37%252Flogo.svg%253Ffit%253Dmax%2526auto%253Dformat%2526n%253DwRG-LXpRtE5ezy37%2526q%253D85%2526s%253De2085fd83535ff0dabc9a8585f14683b%26primaryColor%3D%2523000000%26lightColor%3D%2523ffffff%26darkColor%3D%2523000000%26backgroundLight%3D%2523ffffff%26backgroundDark%3D%25230e0e10&w=1200&q=100)](https://docs.mercurjs.com/integrations/stripe?ref=funkyton.com#stripe-payments) ### Resend [Resend email service - MercurJS documentation![](https://static.ghost.org/v5.0.0/images/link-icon.svg)MercurJS documentation![](https://mercur.mintlify.app/mintlify-assets/_next/image?url=%2F_mintlify%2Fapi%2Fog%3Fdivision%3DIntegrations%26title%3DResend%2Bemail%2Bservice%26logoLight%3Dhttps%253A%252F%252Fmintcdn.com%252Fmercur%252FwRG-LXpRtE5ezy37%252Flogo_dark.svg%253Ffit%253Dmax%2526auto%253Dformat%2526n%253DwRG-LXpRtE5ezy37%2526q%253D85%2526s%253D8616f2bf1a219d2cd10a9f88f03b253d%26logoDark%3Dhttps%253A%252F%252Fmintcdn.com%252Fmercur%252FwRG-LXpRtE5ezy37%252Flogo.svg%253Ffit%253Dmax%2526auto%253Dformat%2526n%253DwRG-LXpRtE5ezy37%2526q%253D85%2526s%253De2085fd83535ff0dabc9a8585f14683b%26primaryColor%3D%2523000000%26lightColor%3D%2523ffffff%26darkColor%3D%2523000000%26backgroundLight%3D%2523ffffff%26backgroundDark%3D%25230e0e10&w=1200&q=100)](https://docs.mercurjs.com/integrations/resend?ref=funkyton.com#resend-email-service) ### Got Questions or Need Help? For quick or simple questions, you’re welcome to leave a comment on the YouTube video. If you're dealing with something more complex, like sharing logs, screenshots, or troubleshooting setup issues, post your question in the [discussion forum dedicated to the deploy template](https://station.railway.com/templates/mercurjs-10ceb1ef?ref=funkyton.com). ### Direct Solana Payments for MedusaJS: Now in Public Beta! URL: https://funkyton.com/medusa-payment-solana/ Last updated: 2026-08-29T20:36:27.000Z If you are curious about how the module works under the hood, why I decided to go down this rabbit hole, and all the design decisions that went into it, please [read my earlier article](https://funkyton.com/medusa-payment-solana-preview/), which dives much deeper into the payment module’s architecture with a supporting sequence diagram. This article will focus on getting a MedusaJS webshop up and running with Solana checkout. If you don’t already know what MedusaJS is, [you can read about it here](https://funkyton.com/medusajs-2-0-is-finally-here/). ### Thanks to all the closed beta testers In the last article, I offered early access to anyone interested. A total of four individual developers and teams reached out and were granted access. Thank you to all of you who took the time to discuss design choices, share ideas, and provide valuable feedback. ## Official release - [https://www.npmjs.com/package/medusa-payment-solana](https://www.npmjs.com/package/medusa-payment-solana?ref=funkyton.com) - [https://github.com/rpuls/medusa-payment-solana](https://github.com/rpuls/medusa-payment-solana?ref=funkyton.com) ## How to set up, step by step. To make your life easy, we are going to deploy a fully preconfigured MedusaJS boilerplate on Railway. This template has been maintained by me for over a year and is loved by many. It will get you a fully operational webshop in just a few minutes. Once deployed, you’ll need to take a few steps to switch to a branch where the Solana payment module is integrated. *If you already have a MedusaJS webshop or want to test the module locally instead of on Railway, this guide is a start-from-nothing setup and may not cover all details. However, it can still serve as a useful reference alongside the* [*module’s README*](https://www.npmjs.com/package/medusa-payment-solana?ref=funkyton.com) *if you are familiar with the Medusa ecosystem.* ### Video instructions ### Step 1: Deploy the Medusa 2.0 Template on Railway You will need a GitHub account and a Railway account with at least the hobby user tier (5$/month). If you don't already have a Railway user, make you to "sign up with GitHub" this will link your user accounts, which is required in the next step. [ ![Deploy on Railway](https://railway.app/button.svg) ](https://railway.com/deploy/medusajs-2-0-storefront?referralCode=-Yg50p&ref=funkyton.com) Click the button above to deploy the standard MedusaJS 2.0 webshop starter. You do not need to configure any of the services, just click deploy and wait for the build to finish - this should take 5-10 minutes. ### Step 2: Eject from the source repo to your own fork On your Railway project canvas, select the "Backend" service with the purple MedusaJS logo. Once selected, go to the "Settings" tab in the right menu. On the settings page, under "Upstream repo," find the red "Eject" button and click it. A pop-up will appear showing your GitHub username. *If you don't see your GitHub user, your accounts are not correctly linked. In that case, click your user avatar in the top right corner, go to "Account Settings," scroll down to "Account Integrations," and connect your GitHub account. Then return to your project and click the eject button again.* Select your GitHub user in the pop-up window to start the ejection process. This will take a few seconds and will eject both the Backend and the Storefront from the official boilerplate repo, creating a fork under your GitHub account and connecting both services to your fork. ### **Step 3: Add the solana-pay Branch to Your Fork** While still on the Settings page of the Backend service, note that the "Source repo" now points to *your* fork. Click it to go to your new repository. You need to clone this repository to your local machine to add the solana-pay branch. At least, I couldn't find another way to do it. Run the following git commands to add the branch to your fork: ```Bash git clone cd medusajs-2.0-for-railway-boilerplate/ git remote add upstream https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate.git git fetch upstream git checkout -b solana-pay upstream/solana-pay git push origin solana-pay ``` Once pushed, you should see the solana-pay branch on your repo after refreshing the page. Don't close the terminal just yet, you will need it again very soon. ### Step 4: Switch Branch on Railway Now that your forked repo contains the solana-pay branch, you can point your Railway services (Backend and Storefront) to this branch. Switch back to the Railway dashboard and refresh your browser to fetch the new branch. While still on the Backend Settings page, scroll down to the "Branch connected to production" dropdown and switch it from "main" to "solana-pay." Repeat this for the Storefront service. This will trigger indicators showing two changes and prompt you to deploy those changes. Do not deploy yet. A few more changes need to be made before deploying anything. ### Step 5: Backend Environment Variables The payment module requires a few environment variables that you need to configure. Below is a template for the variables, but you must replace the placeholders with your own values. This section will explain how to obtain each value. ``` SOLANA_RPC_URL=https://api.devnet.solana.com SOLANA_MNEMONIC="word word word word word word word word word word word word word word word word word word word word" SOLANA_COLD_STORAGE_WALLET= COINGECK_API_KEY= ``` It is highly recommended to start on **devnet** while testing. The `SOLANA_RPC_URL` value above points to devnet and can be changed later to connect to mainnet when ready. For `SOLANA_MNEMONIC`, this is your 20-word passphrase used to generate one-time wallets for each transaction. It must remain private and secure because it is also used to sign transactions when moving funds to your cold storage wallet. If you don’t know how to generate a passphrase, the module includes a script to do so. Remember when you were told not to close the terminal at the end of Step 3? Now switch to that terminal and run: ``` cd backend/ npm install node node_modules/medusa-payment-solana/dist/script/generatePassPhrase.js ``` This will generate a unique 20-word passphrase for you. For `SOLANA_COLD_STORAGE_WALLET`, use a valid wallet address on the Solana devnet. Phantom is a commonly used wallet for devnet if you need a recommendation. Use the address of your secure wallet where you want to receive the funds. To convert currency from EUR or USD to SOL, the module relies on a real-time currency conversion API, currently supported only via Coingecko. To get your own `COINGECK_API_KEY`, you can ask ChatGPT how to obtain an API key for Coingecko. *Comment at the end of this article if you know of any better conversion APIs I should consider adding support for!* ### Step 6: Enable Solana in the Medusa Admin Dashboard The last step is to configure your backend to accept Solana as a payment method. Go to your Medusa Admin dashboard. You can find the URL in Railway by selecting the Backend service and opening the "Deployments" tab. You will need to add `/app` at the end of the URL so it looks something like this: `https://ghost-production-1234.up.railway.app/app` On the login page, enter your admin email and password. These credentials are auto-generated during deployment and can be found in the Railway Backend service's "Variables" tab. Look for `MEDUSA_ADMIN_EMAIL` and `MEDUSA_ADMIN_PASSWORD`. After logging in, navigate to **Settings** \-> **Regions** \-> click **Edit** (...) on your region. Within your region settings, open the payment method dropdown, add **Solana**, and then click **Save**. ### Done 🥳 That's all for the setup. Now you can open your Storefront and perform a checkout, where you should see the Solana payment option available. ## How It Works - **Fiat-first:** Store prices stay in EUR/USD. The module fetches live crypto prices and converts the total cart price at checkout. - **Unique, one-time wallet per order:** A new one-time-wallet address is generated for each order checkout. - **QR code checkout:** Storefront will display the wallet as a QR code for easy mobile wallet payment completion. - **Automatic detection:** A scheduled job tracks the blockchain and captures payments as soon as they’re received. - **Cold wallet security:** Once complete, funds move to cold storage for added safety. - **Automatic price renewal:** By default, every 5 minutes, a new price will be calculated to account for crypto price volatility. *Visual diagram of the updated workflow coming soon.* ## **Live Price Updates Now Supported!** The first demo highlighted a tricky missing piece: **payment session expiration**. - *What if a customer starts checkout at one SOL price, but pays later after the exchange rate swings?* - *What if they send just part of the SOL, or pay just after a session “expires”?* **It’s more complex than a simple timeout.** In reality, partial/late/underpayments are common and need graceful handling, otherwise, users lose payments or get frustrated asking for support. ## The Solution in Beta After testing, feedback, and rethinking, the latest release supports: - **Session pricing windows:** By default, the conversion is locked for 5 minutes, but this is fully configurable. - **Smart partial payments:** All partial transfers are credited to an ongoing session field (`received_sol_amount`), allowing for multiple payments until the full due amount is tracked. - **Automatic order placement:** Once the session’s full amount is met or exceeded, the module finalizes and places the order. - **Resilience for late/underpayments:** Users are never asked to “start over” and lose their funds, outstanding balances, and new prices are dynamically calculated. ## Security vs. Privacy Challenge (TBD) Currently, the module requires a cold storage wallet for automatic stashing (security first). However, someone reached out and pointed out that this compromises privacy, as the cold wallet address can be tracked by following the one-time wallet address and seeing where it next transfers its balance. This challenge has not been resolved in the beta release. The current planned solution is to modify the module to accept an array of cold storage wallet addresses. This allows stores to either use no cold storage wallet, keeping funds on the one-time wallets for full privacy, or use multiple cold storage wallets to provide some level of masking (more secure, but less privacy and more administration). If you know of any better way to tackle this issue that doesn’t involve third-party services, please reach out to me! If you have questions or want to contribute, reach out anytime! You can also check out the [module’s roadmap here](https://github.com/users/rpuls/projects/2?ref=funkyton.com). ## Disclaimer ⚠️ This Solana payment module is (or will be) provided as free, open-source software, with no warranties, guarantees, or liability of any kind. Use it at your own risk. I do not charge for this module, nor do I offer insurance, customer support, or any guarantees against software bugs, user mistakes, or lost funds. Always test thoroughly on testnet before using it with real funds, and make sure you understand how blockchain payments work. It is your responsibility to ensure that your use of this software complies with all applicable laws and regulations in your country or jurisdiction, including but not limited to anti-money laundering (AML) and cryptocurrency regulations. I am not liable for any legal issues, penalties, or consequences resulting from the use or misuse of this software. By using this module, you accept full responsibility for any transactions, losses, errors, or legal issues that may occur. If you have questions or need help, feel free to reach out-but please remember: you are ultimately responsible for your own funds, compliance, and security. ### Building a Direct Solana Payment Module for MedusaJS 2: No Middlemen, Just Blockchain URL: https://funkyton.com/medusa-payment-solana-preview/ Last updated: 2026-02-17T17:16:45.000Z Most crypto payment solutions available today are built around third-party processors, bringing extra fees, KYC hurdles, and limited control, far from the original DeFi vision of direct, peer-to-peer payments powered by code. This project is about exploring what's technically possible when you cut out the middleman: a truly decentralized Solana payment module for MedusaJS 2\. In this post, I'll dive into the architecture, share a live demo, and invite you to get involved as an early tester. This is about what crypto was always meant to be, no gatekeepers, just code enabling commerce between two parties. 📺 OBS: Video demo, later in post. ## What is MedusaJS (and why should you care) ? MedusaJS is an open-source, headless commerce platform that gives developers full control over their e-commerce stack, making it ideal for building custom payment modules and other extensions. **Why It Matters** If you're a developer tired of rigid platforms like Shopify, MedusaJS lets you compose your backend exactly as needed, with modular APIs for payments, inventory, and more. It's flexible, scalable, and free to customize. **Quick Start** Want to launch MedusaJS 2 in minutes? Use my [one-click deploy template](https://funkyton.com/medusajs-2-0-is-finally-here/) for a fully operational webshop, ready to tweak. ## Why build a Solana payment module? When people talk about cryptocurrency's potential, they often mention how it enables direct peer-to-peer transactions without intermediaries. Yet ironically, most "crypto payment" solutions still route through third-party processors that: - Charge substantial fees. - Require KYC verification. - Apply geoblocks. - Offer poor exchange rates. - Can change terms or block merchants at will. I wanted to build something that delivers on crypto's original promise, **truly direct payments**. Here's why Solana made sense: 1. **Speed and cost**: Transactions confirm in seconds and cost fractions of a cent. 2. **Independence**: No reliance on payment processors that can suddenly change terms. 3. **Resilience**: An alternative payment rail if traditional systems are unavailable. 4. **Because it's cool**: Let's be honest, building this was just technically fascinating. ## How it works: The payment flow Here's the sequence diagram showing exactly how payments move through the system: ![Sequence Diagram over Payment Flow for Solana Blockchain Payments in MedusaJS](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/payment-sequence-diagram-solana-crypto-payment-in-medusajs-png.png?_a=BAMAJafi0) The magic happens in these key steps: 1. When a customer selects "Solana Pay," the system converts the cart total from fiat (EUR) to SOL using real-time rates. 2. A **one-time wallet address** is generated specifically for this transaction. 3. The customer sends SOL to this address using their wallet (like Phantom), by scanning a QR code. 4. A scheduled job continuously monitors the blockchain for incoming payments. 5. When payment is detected, it's automatically authorized and captured. 6. The system transfers funds from the one-time wallet to cold storage. 7. The order is confirmed and the customer sees the confirmation page and receives a confirmation e-mail. ## Technical innovations (and challenges) ## The one-time wallet system: Security, Privacy, and Simplicity A core innovation in this module is that **every payment gets its own unique, one-time Solana wallet address**. This isn’t just a fun technical trick-it solves several real problems for both merchants and customers: - **Security**: By generating a fresh wallet address for each order, you eliminate exposure of your main wallet. Even if someone tries to attack or spam a payment address, it only affects that single transaction-not your entire treasury. - **Privacy**: If you reused the same wallet address for all orders, anyone (customers, competitors, random crypto sleuths) could look up that address on the Solana blockchain. They’d instantly see your entire transaction history and current balance. That’s like putting your bank statement on a public billboard-not exactly ideal for any webshop. One-time wallets mean each order is isolated, so nobody can snoop on your overall business by tracking a single address. - **Order-to-Transaction Clarity**: With a unique wallet per payment, it’s always clear which blockchain transaction belongs to which order. There’s no guesswork or ambiguity-when funds hit a specific one-time address, the module knows *exactly* which cart or payment session it corresponds to. - **No Manual Checking Needed**: In traditional crypto payment setups, someone often has to log into a wallet, check incoming transactions, and then mark orders as paid. That’s tedious, error-prone, and doesn’t scale. With this system, the module automatically watches each one-time wallet address, and as soon as the payment arrives, it triggers the next steps in your order flow-all hands-off, all instant. This approach is all about making crypto payments as seamless, private, and reliable as possible for both shop owners and customers. ## Real-time currency conversion I made a deliberate design choice to keep product pricing in fiat currency (which is also the MedusaJS default), rather than in crypto. Here’s why: - **Price stability**: Crypto prices are notoriously volatile-imagine having to update all your product prices not just daily, but potentially every hour! Keeping prices in fiat means you don’t have to chase the market. - **Business reality**: Most merchants pay suppliers, employees, and taxes in fiat. Pricing products in the same currency you use for expenses just makes life simpler and accounting more straightforward. - **Compatibility**: This approach lets you offer crypto payments as an option, while still supporting traditional payment methods like [credit cards (via Stripe)](https://funkyton.com/add-stripe-payment-module-to-medusajs-2-0/) or PayPal. No need to pick one or the other-you can have both. The module takes care of converting the fiat total to SOL in real time using external APIs, so your customers always see the current rate at checkout. This way, you get the best of both worlds: stable pricing for your business, and flexible payment options for your customers. ## The payment expiration challenge ✅ UPDATE: Solved in open beta release: [https://www.npmjs.com/package/medusa-payment-solana](https://www.npmjs.com/package/medusa-payment-solana?ref=funkyton.com) One critical piece I'm still working on is **payment session expiration**. Without this, there’s a risk that customers could exploit crypto price swings to get unfair discounts. Let's break down how this could happen with a concrete example: Suppose your store uses EUR as the base currency and a customer fills their cart with €150 worth of products. At that moment, the conversion rate is **1 SOL = €150**, so the checkout shows the customer needs to pay 1 SOL. But here’s the catch: If the customer waits a few days (or even weeks) and the SOL price drops, so now **1 SOL = €100**, they could buy 1 SOL for just €100 and complete the payment session they started earlier. They’d get €150 worth of products for only €100-worth of SOL, just by waiting for the right moment. That’s a €50 discount simply from timing the market, not from any sale or promotion you intended. To prevent this kind of arbitrage, I’m building a session expiration system. Payment sessions will only be valid for a configurable time window (for example, 15 or 30 minutes). After that, the session expires, and storefront (webshop frontend app) will request an updated price for the payment session. ## Current status: Demo time Today I'm releasing a video demo showing the current state of the module. In the demo, I'll walk through: - A complete checkout flow on the Solana testnet. - Payment via Phantom wallet on my phone (testnet). - Real-time order confirmation after blockchain verification. The core payment flow is working, but there's still work to be done before public release. ## Roadmap: What's next? Before releasing this module publicly, I'm focusing on: 1. **Payment session expiration**: As mentioned above. 2. **Enhanced error handling**: For network issues and edge cases. 3. **Admin dashboard integration**: For better merchant visibility such as; blockchain transaction details. 4. **Documentation**: Comprehensive setup guides. ## Want early access? If you're interested in testing this Solana payment module before its public release: - Reach out, my e-mail can be found at the [info page](https://funkyton.com/info/). - Let me know what kind of store you're running Early testers will get priority support and influence the direction of the module's development. ## The bigger picture This project represents more than just a payment module-it's about making cryptocurrency payments practical for everyday e-commerce. By removing intermediaries and simplifying integration, we're getting closer to the original vision of cryptocurrency as a direct payment method. Whether you're a MedusaJS developer looking to offer more payment options, a crypto enthusiast wanting to support blockchain adoption, an e-commerce store owner seeking payment independence, or just a geek like me who finds it fascinating that you can use nothing but code, no banks, no card companies, just code, to process a trade between two parties, I'd love to hear your thoughts and questions. Please comment below 👇 ## Disclaimer ⚠️ This Solana payment module is (or will be) provided as free, open-source software, with no warranties, guarantees, or liability of any kind. Use it at your own risk. I do not charge for this module, nor do I offer insurance, customer support, or any guarantees against software bugs, user mistakes, or lost funds. Always test thoroughly on testnet before using it with real funds, and make sure you understand how blockchain payments work. It is your responsibility to ensure that your use of this software complies with all applicable laws and regulations in your country or jurisdiction, including but not limited to anti-money laundering (AML) and cryptocurrency regulations. I am not liable for any legal issues, penalties, or consequences resulting from the use or misuse of this software. By using this module, you accept full responsibility for any transactions, losses, errors, or legal issues that may occur. If you have questions or need help, feel free to reach out-but please remember: you are ultimately responsible for your own funds, compliance, and security. ### How to Run an OWASP ZAP Vulnerability Scan Online Without Local Installation URL: https://funkyton.com/zap-owasp-web-scan/ Last updated: 2026-02-26T13:49:40.000Z ## Video demonstartion Both videos for local and cloud setup can be found later in the article. ### The Compliance Irony I recently found myself in an ironic and mildly frustrating situation: tasked with performing a yearly [OWASP](https://owasp.org/?ref=funkyton.com) scan as part of a security policy, only to discover that the very same policy prevented me from installing the software needed to run the scan. (Ah, the joys of ISO 27001 compliance, am I right?) As it turns out, the policy prohibits installing software flagged by antivirus programs or sourced from unofficial platforms. Unfortunately, Windows Defender had deemed the ZAPROXY software, which is essential for conducting OWASP vulnerability scans, as unsafe. This left me in a frustrating deadlock, required to complete the scan to comply with the policy, yet unable to install the tool because of that very same policy. > [ZAP](https://www.zaproxy.org/?ref=funkyton.com) stands for Zed Attack Proxy, and is The world’s most widely used web app scanner. Free and open source. Anyone can and should use ZAP scans frequently, to detect vulnerabilities. ### Alternative ways to run ZAP Thankfully, ZAP offers their open-source software as a [Docker image](https://hub.docker.com/r/zaproxy/zap-stable?ref=funkyton.com), which turned out to be a lifesaver. This meant I could simply rent a cloud machine and run the software there instead of on my workstation. By using a cloud instance, I could perform the vulnerability scan of any web application through the ZAP API while staying fully compliant with the policies. I decided to launch the ZAP Docker image on Railway, which took just a few seconds to spin up. Unfortunately, I hit a roadblock right away. The API wouldn’t work over HTTPS, and Railway’s public URLs don’t allow HTTP. A bit of a showstopper... So, I had to improvise. The only solution I could think of was to create an ultra-simple Node.js app and launch it within the same Railway project. This Node service can then communicate with the ZAP API over Railway’s internal network, which uses the HTTP protocol. With this setup, I was able to use the API, perform the scan, and get the results in "beautiful" unformatted JSON. Job done! ### Creating a simple User Interface However, being a bit of a perfectionist, I couldn’t help myself. Later the same day, I started building a very simple web interface for the Node.js service. The idea was to make it easier for future scans: next time I need to do an OWASP scan, I can simply visit the web interface, paste in the URL of the website I want to scan, and get the results delivered directly in the browser. In the screenshot below, you can see a preview of the web interface after completing a scan of this very blog. ![Web application user interface of ZAP OWASP showing completed scan results](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/ZAP-OWASP-website-scanner-application-interface.png) Web application user interface of ZAP OWASP showing completed scan results As you might have noticed from the screenshot, I didn’t stop at creating a simple web interface. I also decided to add a PDF report downloader, so I could save the scan results as evidence for ISO27001 compliance. PDF documents are far easier to digest than raw JSON, so I thought this would be a nice addition. That said, the PDF report service still needs some work on layout and formatting. It’s not exactly a masterpiece, it looks more like something you’d get from a public sector entity, but for now, it gets the job done! If you’d like to use my app to perform an OWASP scan yourself, I’ve made it incredibly easy. I’ve created a 1-click deploy template that allows you to set up your own exact copy of this project on a cloud machine in just a few minutes. Simply click the purple button further down the page to access the deploy page. ### ZAP Hardware Requirement But, before you dive in, I should mention that the ZAP Docker service is quite memory-hungry. It requires at least 2GB of RAM to run, but I’ve seen my instance use up to 4GB after scanning a website, and potentially even more for larger scans. After asking around, I learned this is completely normal; it’s just how ZAP works. For this reason, you likely won’t be able to perform scans on Railway’s free user tier, as it limits services to 500MB of RAM, which, in my testing, isn’t enough to complete a scan. To make it work, you’ll need at least the Hobby plan, which costs $5/month. Below is a screenshot of the resource usage for the ZAP container, where you can see it peaking above 2GB. ![Resource use on Railway of ZAP docker container, high memory use](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/ZAP-docker-service-resource-useage-metrics-memory-cpu.png) ## How to Deploy and Use the ZAP Scanner As promised, here’s the link to the deploy template. You’ll need an account to get started, and the easiest way is to sign in with GitHub. Click the button below to go to the deploy page. [ ![Deploy on Railway](https://railway.app/button.svg) ](https://railway.com/deploy/dCh187?referralCode=-Yg50p&ref=funkyton.com) Once you start the deployment, it should take about 2–3 minutes to spin up both the Docker image and the Node.js app with the web interface. When you see green checkmarks on both services, select the one called **"Web UI"** and go to the **"Deployments"** tab. Here, you’ll find your public URL. Click it to visit your live web interface. ![How to find the public URL where your web app is hosted](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/How-to-access-your-ZAP-web-interface-from-Railway.png) ### Local Setup Instead of Cloud If you don’t have a Railway account or prefer not to use the cloud setup, you can easily run the app locally. Here’s what you’ll need to do: 1. **Run the ZAP Docker Image Locally**: Ensure you have Docker installed on your machine and run the official OWASP ZAP Docker image. 2. **Start the Node.js Application**: Clone the project repository and run the Node.js app locally. For detailed setup instructions, please refer to the [GitHub README](https://github.com/rpuls/ZAP-OWASP-Web-interface?ref=funkyton.com). Once everything is up and running, you can access the web interface by visiting `localhost:3000` in your browser. ### Performing a ZAP Scan Once your web UI has loaded (cloud or local), you’ll find the scan page. ![Enter URL and press start scan to begin ZAP vulnerbility scan](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/enter-url-and-start-ZAP-scan.png) The interface is pretty straightforward: enter the URL of the website you want to scan and press the **"Start Scan"** button. This triggers a request to the Node.js backend, which handles the rest. First, it caches your scan request, then it uses the ZAP API to start a spider scan. The spider scan crawls the website for links and assets, mapping out all areas to test for vulnerabilities. It’s a quick process and usually only takes a few seconds. Once completed, the web interface updates to show a progress bar. While the progress bar is running, the ZAP software performs an "active scan". This is a more advanced operation that tests for vulnerabilities in greater depth. It’s quite sophisticated and involves techniques beyond what I can comfortably explain in this blog post, but all you need to know is that it can take some time, **sometimes several minutes**. Once the scan is complete, you’ll see your results immediately, as shown in the preview screenshot from earlier. ## Understanding your results The results (also called alerts) are categorized by severity, with **High** being the most critical vulnerability, something you should likely pay close attention to. Additionally, there are **Medium**, **Low**, and **Informational** alerts. However, just because something is flagged as an alert doesn’t necessarily mean it’s a security vulnerability, so don’t panic when you see alerts in your scan results. Let’s take a look at an example. ![Example of alert from ZAP scan result, expanded with details and solution](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Expand-ZAP-alert-to-inspect-it-for-details-and-solution.png) As you can see, the alert is structured in a helpful way. It not only explains what’s wrong but also provides hints on how to resolve it, or even ignore it if you determine it’s not an actual vulnerability. Here’s how the information is typically organized: 1. **Alert Description**: Each alert represents a potential security vulnerability or issue. In this case, the alert is about the Content Security Policy (CSP) using wildcard directives, which can weaken security. 2. **Severity Levels**: Alerts are categorized by severity: High (red), Medium (orange), Low (yellow), and Informational (blue). This helps you prioritize which issues need immediate attention. 3. **Description**: Explains why the issue is problematic, such as how wildcard sources in CSP can introduce risks. 4. **Solution**: Offers suggestions to fix the issue, like configuring the CSP header more securely. 5. **Evidence**: Provides specific examples from your application that triggered the alert, such as the detected CSP configuration. 6. **URL Affected**: Lists the URL where the vulnerability was found, helping you pinpoint where to apply fixes. 7. **References**: Includes links to resources for further reading and understanding of the issue. The last three points; Evidence, URL Affected, and References, are particularly useful for tracking down the source of an issue if you decide it needs to be addressed. If you encounter an alert that’s hard to interpret, I recommend copying all the text and pasting it into your favorite LLM with a prompt like:*"This is a ZAP alert. Help me interpret it: "* From there, you can discuss your web app’s construction with the AI to determine whether the alert needs addressing and how to resolve it. Alternatively, you can consult an external security advisor if you're still unsure, but keep in mind this option can get costly quickly. ## How to become ISO 27001 compliant? I recently went through the process of achieving ISO 27001 compliance with the company I work for in my day job. In our team, we share responsibilities, and I was assigned the task of conducting the yearly OWASP scan, something we decided to include when drafting our policy documents. While OWASP scans are not a strict requirement under the ISO 27001 standard, I can’t think of a good reason not to do them. One of the great things about ISO 27001 is its flexibility. It’s not a rigid list of rules you’re forced to follow; instead, you define policies that make sense for your company. However, crafting these policies in a way that aligns with your organization’s needs is no small feat! Thankfully, we had significant help from [CyberJuice](https://cyberjuice.io/?ref=funkyton.com), a company that offers frequent webinar courses on ISO compliance and provides an online platform packed with useful tools. A standout feature of their platform is the template editor. It offers standard policy templates that you can easily customize to fit your company’s requirements. This was a huge time saver, considering you need to create over 10 documents, writing everything from scratch would have been a monumental task. With their support, it took us six weeks to complete the process. We held weekly online meetings and tackled some homework assignments in between. While I wouldn’t call the process fun, it was definitely manageable. You’ll likely discover, as we did, that you don’t need to drastically change your routines to achieve compliance. If you’re already mindful of IT security best practices, chances are you’re doing many of the right things already. ISO 27001 is primarily about documenting those habits and routines so they’re formalized on paper. The day-to-day overhead isn’t as bad as it might seem at first. ## Final thoughts This was a fun little hobby project, building the Node.js interface app came out of a real-world task: performing a yearly OWASP scan. Since I put this together in my free time over just a couple of days, please don’t expect too much from this app. It’s nothing like the full-featured desktop application that ZAP offers for Windows, Linux, and MacOS, which includes many more advanced capabilities. However, in my case, I couldn’t install the desktop version due to Windows Defender and the security policies I work under. Sometimes, simple is enough. While I’m only using a fraction of what the ZAP API can do, it fully meets my need to perform vulnerability scans. Looking ahead, I see potential to integrate an AI service to summarize scan results. Another feature that I would like to add, is a scheduler for auto scans, with result comparions, this way you can get notified rapidly if a new alert is discovered. These additional feature could easily be added to the project and would offer functionality that the ZAP desktop application doesn’t provide, making this "easy-to-use version" even more relevant. I hope you enjoyed reading about this project! If you have ideas for new features or improvements, feel free to make a pull request on the [Git repository](https://github.com/rpuls/ZAP-OWASP-Web-interface?ref=funkyton.com) and contribute. Alternatively, if you have suggestions or questions, you’re more than welcome to leave a comment below. ## Disclaimer I am not affiliated with or endorsed by CyberJuice, ZAP, or OWASP. These are personal recommendations based solely on my first-hand experience. ### How to Add MeiliSearch to Your Medusa 2.0 Project URL: https://funkyton.com/how-to-add-meilisearch-to-your-medusa-2-0-project-2/ Last updated: 2026-08-29T20:37:13.000Z ⚠️ UPDATE: For plugin version 1.4+ the integration is a plugin, not a module, and no subscribers are needed. Older setup? See the upgrade note at the end. [Github README for more info](https://github.com/rokmohar/medusa-plugin-meilisearch?tab=readme-ov-file&ref=funkyton.com#meilisearch-plugin-for-medusa-v2). This guide was updated on 21\. July 2026 This guide assumes you already have a working Medusa 2.0 project with both a backend and storefront set up. We'll walk you through implementing MeiliSearch step by step. If you'd rather skip the manual setup, you can use my Railway template for an effortless deployment where everything is preconfigured: [ ![Deploy on Railway](https://railway.app/button.svg) (OBS: requires hobby plan 5$/m) ](https://railway.com/deploy/medusajs-2-0-storefront?referralCode=-Yg50p&ref=funkyton.com) > *Railway caps free users to 5 services. The template above has 7\. I also have a* [*TRIAL version*](https://railway.app/template/Fb6KUX?referralCode=-Yg50p&ref=funkyton.com) *that does *not* include MeiliSearch.* ### Video Instructions ℹ️ (Note: the video shows an older version of the integration. The steps are similar, but follow the written guide below for the current plugin configuration.) If you're ready to dive into the implementation yourself, or if you just want to understand how it is implemented in the template above, this guide will cover it all, in three simple steps: 1. Run a MeiliSearch instance 2. Implement MeiliSearch in the Medusa backend 3. Configure MeiliSearch in the storefront ## 1\. Run a MeiliSearch Instance MeiliSearch is a powerful, open-source search engine that runs as a separate service, not just an npm dependency. Below are three methods to run it: **Railway**, **Docker**, or [**MeiliSearch Cloud**](https://www.meilisearch.com/cloud?ref=funkyton.com). We'll cover the first two in detail. ### **Method 1: Railway** 1. Go to your project canvas on Railway and create a new service. 2. Choose the **Docker Image** option and search for `getmeili/meilisearch:latest`. ![Add MeiliSearch docker image to your Railway project canvas](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/create-MeiliSearch-service-on-railway.gif) Add MeiliSearch docker image to your Railway project canvas 1. Once deployed: - Rename the service to `MeiliSearch`. - Navigate to the **Variables tab** and add the following environment variables: ``` MEILI_DB_PATH="/meili_data/data.ms" MEILI_ENV="production" MEILI_HTTP_ADDR=":::3331" MEILI_MASTER_KEY="some-long-random-string" MEILI_PUBLIC_URL="${{RAILWAY_PUBLIC_DOMAIN}}" MEILI_MAX_INDEXING_MEMORY="2GiB" PORT="3331" ``` 1. Attach storage volume, right click on the MeiliSearch container and choose Attach Volume from the menu ![Attach storage volume to your MeiliSearch Railway service](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Railway-MeiliSearch-service-attach-storage-volume.png) Attach storage volume to your MeiliSearch Railway service 1. Set mount path to `/meili_data` ![MeiliSearch storage volume mount path](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Railway-MeiliSearch-storage-volume-mount-path.png) MeiliSearch storage volume mount path 1. Enable networking, go to **Settings** \-> **Networking** and turn on **HTTP** ![Enable networking on MeiliSearch Railway service](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Meilisearch-on-railway-set-networking-to-HTTP.png) Enable networking on MeiliSearch Railway service That's it! Your MeiliSearch instance is now ready. You can skip the Docker section now that you've used Railway instead. Use image below for reference your finished result. ![Finished configuration of MeiliSearch service on Railway](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/MeiliSearch-service-configured-on-railway.png) Finished configuration of MeiliSearch service on Railway ### **Method 2: Docker (Local or VPS)** Use the following Docker Compose configuration to set up MeiliSearch locally or on a VPS: `docker-compose.yml` ``` services: meilisearch: image: getmeili/meilisearch:v1.49.0 ports: - "7700:7700" volumes: - meilisearch-data:/meili_data environment: MEILI_MASTER_KEY: your_master_key_here healthcheck: test: ["CMD-SHELL", "curl -fs http://localhost:7700/health || exit 1"] interval: 30s timeout: 5s retries: 5 volumes: meilisearch-data: ``` 💡 Tip: if you are using my [Railway boilerplate,](https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate?ref=funkyton.com) you can skip this. The repository now ships a docker-compose.yml in the root that includes MeiliSearch (plus Postgres, Redis and bucket storage) preconfigured for local development. If you are not familiar with Docker compose, you may refer to [the quick start guide](https://docs.docker.com/compose/gettingstarted/?ref=funkyton.com). Additionally, more information about local instance of MeiliSearch can be found [here](https://www.meilisearch.com/docs/learn/self%5Fhosted/install%5Fmeilisearch%5Flocally?ref=funkyton.com). ### **Method 3: MeiliSearch Cloud** If you prefer a managed solution or want to support the developers, consider using [MeiliSearch Cloud](https://www.meilisearch.com/?ref=funkyton.com). It provides seamless deployment and additional features like facet search, semantic search, and geosearch. [Meilisearch | CloudMeilisearch: A powerful, open-source search engine offering fast and relevant full-text searches. Enhance your search capabilities with features like facet search, semantic search, hybrid search, and geosearch. Optimize indexing with best practices and enjoy seamless deployment with Meilisearch Cloud for an improved search experience.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Register with Google![](https://meilisearch.com/og.png)](https://www.meilisearch.com/cloud?ref=funkyton.com) ## 2\. Backend Implementation Since the Medusa core team hasn’t released an official MeiliSearch module for version 2.0 yet, we’ll use the community-created `@rokmohar/medusa-plugin-meilisearch`. Make sure to ⭐ their [repository](https://github.com/rokmohar/medusa-plugin-meilisearch?ref=funkyton.com)! Version compatibility matters here, check the table in the plugin's README. In short: - Plugin ^1.4.1 requires Medusa ^2.15.2 (and Node >= 22) - Plugin ^1.3.7 works with Medusa ^2.13.4- Older Medusa versions need plugin 1.0.x ### Step 1: Install the Plugin Run one of these commands in your Medusa backend directory: ``` npm install --save @rokmohar/medusa-plugin-meilisearch # or yarn add @rokmohar/medusa-plugin-meilisearch ``` ### Step 2: Configure Environment Variables Add these variables to your backend's `.env` file: ``` MEILISEARCH_HOST=http://localhost:7700 # Or public cloud URL if you are using cloud an online MeiliSearch instance (Railway) MEILISEARCH_API_KEY=your_master_key_here ``` And, if you are also hosting your project on **Railway** add the following two variables to your Medusa 2.0 backend Railway service: ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/MeiliSearch-environment-variables-for-medusa-2-backend-on-railway.png) Medusa backend environment variables for MeiliSearch on Railway ``` MEILISEARCH_API_KEY="${{MeiliSearch.MEILI_MASTER_KEY}}" MEILISEARCH_HOST="https://${{MeiliSearch.MEILI_PUBLIC_URL}}" ``` ### Step 3: Update Medusa Configuration In your `medusa-config.js`, add the plugin configuration: ```JavaScript plugins: [ // ... your other plugins, { resolve: '@rokmohar/medusa-plugin-meilisearch', options: { config: { host: process.env.MEILISEARCH_HOST, apiKey: process.env.MEILISEARCH_API_KEY }, settings: { products: { type: 'products', enabled: true, fields: ['id', 'title', 'description', 'handle', 'variant_sku', 'thumbnail'], indexSettings: { searchableAttributes: ['title', 'description', 'variant_sku'], displayedAttributes: ['id', 'handle', 'title', 'description', 'variant_sku', 'thumbnail'], filterableAttributes: ['id', 'handle'], }, primaryKey: 'id', } } } } ] // rest of your config... ``` See how I added it: [https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate/blob/master/backend/medusa-config.js](https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate/blob/master/backend/medusa-config.js?ref=funkyton.com) ## **Step 4: Indexing, you don't need to do anything! 🎉** Older versions of this guide had you write subscribers for product create/update/delete events. That is no longer necessary: the plugin ships with its own subscribers for products, variants, categories and more, plus a one-shot job that performs a full index sync about a minute after the (worker) instance starts. You can also trigger a full re-sync manually at any time from the admin dashboard under Settings -> MeiliSearch, or via POST /admin/meilisearch/sync. One heads-up for split deployments: subscribers and jobs only run in worker mode. If you run separate server and worker instances, the plugin must be registered on both (if you keep it in your shared medusa-config, you're fine). That's all for the backend! 🚀 ## 3\. Storefront Implementation If you are using my boilerplate storefront, there is nothing to implement. Search is already wired up, and as of the July 2026 update it uses @meilisearch/instant-meilisearch 0.31+ together with react-instantsearch v7\. If you built search from an older version of the storefront (or this guide), two breaking changes are worth knowing when you upgrade the packages: 1. instantMeiliSearch() now returns an object, so destructure it: `const { searchClient } = instantMeiliSearch(endpoint, apiKey)` 2. react-instantsearch-hooks-web is deprecated. The same hooks (InstantSearch, useHits, useSearchBox) now live in react-instantsearch v7, so it's mostly a find-and-replace of the import. ### Step 1: Fetch search API key The request you need to make is: ``` GET /keys Authorization: Bearer Content-Type: application/json ``` From the response, locate the `"key"` field where the `"actions"` array includes `"search"`. Copy this value—it is your search-only API key. This key should only be used on the frontend as it doesn't give any sensitive permissions. This key is the only one safe to use in the frontend. Never put your master key or admin key in a `NEXT_PUBLIC_` variable, as those are shipped to every visitor's browser. (If you are using my boilerplate on Railway, you can skip this step. The storefront launcher fetches a search-only key automatically at boot using the `MEILISEARCH_API_KEY` variable.) ### Step 2: Configure Environment Variables Add these variables to `.env.local` in your storefront: ``` # MeiliSearch Configuration NEXT_PUBLIC_SEARCH_ENDPOINT=http://localhost:7700 NEXT_PUBLIC_SEARCH_API_KEY= NEXT_PUBLIC_INDEX_NAME=products ``` And, if you are also hosting your project on **Railway** add the following two variables to your Storefront Railway service: ``` NEXT_PUBLIC_SEARCH_ENDPOINT="https://${{MeiliSearch.MEILI_PUBLIC_URL}}" NEXT_PUBLIC_SEARCH_API_KEY="" NEXT_PUBLIC_INDEX_NAME="products" ``` ### All done! You should now have a fully functional MeiliSearch integration with your Medusa project! Test it out by using the search field in your storefront 🚀 ![Product search made possible with MeiliSearch integration for medusajs](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/search-with-meilisearch-in-medusa-storefront.gif) Product search made possible with MeiliSearch integration for medusajs ### Alternative storefront implementation Below is a simple React component with an input field and a search button that fetches search results from your MeiliSearch container via its [search endpoint](https://www.meilisearch.com/docs/reference/api/search?ref=funkyton.com): ```JSX import React, { useState } from 'react'; const SearchComponent = () => { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); // MeiliSearch URL with environment variable fallback const meiliSearchUrl = process.env.NEXT_PUBLIC_SEARCH_ENDPOINT || 'http://localhost:7700'; const searchApiKey = process.env.NEXT_PUBLIC_SEARCH_API_KEY; // Ensure this is set in your environment const handleSearch = async () => { try { const response = await fetch(`${meiliSearchUrl}/indexes/products/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${searchApiKey}` // Use the search-only API key }, body: JSON.stringify({ q: query }) }); if (!response.ok) { throw new Error('Failed to fetch search results'); } const data = await response.json(); setResults(data.hits); } catch (error) { console.error(error); } }; return (
setQuery(e.target.value)} placeholder='Search for products...' />
    {results.map((result) => (
  • {result.title}
  • // Adjust based on your index structure ))}
); }; export default SearchComponent; ``` In this example, I directly import the API key and MeiliSearch URL from `process.env` within the component body. However, in real-world React development, it's better to pass these values through a centralized configuration or context. This approach ensures better maintainability and makes it easier to manage environment-specific configurations. ### How to Add Comment Section to your Pages with Payload CMS 3 URL: https://funkyton.com/payload-comment-section/ Last updated: 2025-09-05T08:44:26.000Z Learn how to integrate a comment system into your Payload CMS v3 project with React. This guide offers 4 simple steps for creating a server-rendered Comment List and a client-side Comment Form, making it easy to boost user interaction in your web app. Suitable for developers at any level! _This post is for subscribers only._ ### Payload CMS 3 - Open source website builder URL: https://funkyton.com/payload-cms/ Last updated: 2026-05-13T21:00:52.000Z Choosing the right CMS can feel nearly impossible, even when you know exactly what you need. The market is flooded with options, from simple and free to complex and pricey. In this post, I'll give an honest review of Payload CMS, the "new" kid on the block that's been popping up on my radar lately. It's fully open source, which is a huge plus for developers who want flexibility and control. And, as always, if you find this review helpful and want to give Payload a spin, I've made it easier than ever to deploy your own instance, just keep reading for the 1-click deploy template later in the article. ### What to expect In the first part of this article, I'll briefly cover what you can expect from Payload CMS when launched from this template. Right after that, I'll guide you through the simple steps to deploy your own instance of Payload CMS, so you can start building your blog, website, or whatever project you have in mind. Once you've set everything up and created your first pages, I'll introduce you to the world of customizations. You'll quickly see that, no matter what features or functionality you need, Payload can be configured to handle it. Whether it's adding custom content types, integrating third-party APIs, or building dynamic forms and dashboards, Payload offers nearly endless possibilities. Of course, these advanced customizations require some patience and technical work, but you won't hit a hard “no” wall. For those of you running a [MedusaJS webshop](https://funkyton.com/medusajs-2-0-is-finally-here/), I've received many requests asking how to integrate a CMS into your storefront. In an upcoming article, I'll dive into how you can seamlessly integrate this exact Payload CMS setup into your MedusaJS storefront for a fully customizable e-commerce experience. ### Preconfigured key features in this template - Pages - Posts (blog/articles) - Forms - Users (with roles) - Comment system ![Overview of all preconfigured content types in Payload CMS V3 (website builder)](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Payload-CMS-V3-preconfigured-collections-and-globals.png) Overview of all preconfigured content types in Payload CMS V3 (website builder) ### CMS comparison To give you some perspective, I've compared Payload to a few other well-known CMS platforms. I'm well aware that there are many other, often more popular options out there, but I chose these because they cover a broad range of use cases, styles, and technologies. These are platforms I've personally worked with, offering a well-rounded comparison. The following categories are evaluated: - **Ease of Use**: How good is the Payload admin / content manager experience. - **Customization**: This score reflects both the potential for customization and how difficult it is to implement complex changes. (payload developer experience). - **Price (Official)**: The monthly price from the official hosting provider. - **Price (Self-Hosting)**: The estimated monthly cost using my Railway deploy template. This benchmark is based on what I've personally paid to host these templates, but note that high traffic can result in higher prices. | Platform | Ease of Use | Customization | Price (Official) ($/m) | Price (self) ($/m) | Open Source | | ----------------------------------------------------------------------------- | ----------- | ------------- | --------------------------------------------------------------- | ------------------ | ----------- | | [Ghost](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/) | 10/10 | 5/10 | [$18-$239](https://ghost.org/pricing/?ref=funkyton.com) | $7 | ✅ | | Squarespace | 8/10 | \- | [€17-€79](https://www.squarespace.com/pricing?ref=funkyton.com) | \- | ❌ | | WordPress | 7.5/10 | 4/10 | [€9-€70](https://wordpress.com/pricing/?ref=funkyton.com) | \- | ✅ | | Payload | 7.5/10 | 9/10 | $35 | $7 | ✅ | **About ratings**: While WordPress offers extensive customization, it's built on a language that has seen better days, and making changes often comes with frustration. Ghost is the undisputed champion for ease of use—nothing beats how quickly you can write your first post or page. However, as soon as you need more advanced features, you'll likely outgrow it. This is where Payload shines, offering much greater flexibility for adding complex custom features. As for pricing, I only provide deploy templates for two platforms, so if you're looking for ultra-low-cost self-hosting, there's really only two choices. *Please note that railway hosting prices is per use, so if you have high traffic on your project you might need to pay more. The prices in the table is what I pay, with low to moderate traffic on my sites.* ### How to get started, building with Payload in minutes This boilerplate is set up as a website builder, perfect for writing articles, personal portfolio, blogging, or showcasing your product or business. The CMS is easy and intuitive to use right out of the box, so you can get started quickly. If you need more advanced features, Payload offers virtually unlimited customization options, though adding these will require a higher level of technical involvement and some tinkering. You can find my customization guide at the bottom of the article. To get started, launch the template on Railway by clicking the button below, and follow the steps as shown on the screenshots. [ ![Deploy on Railway](https://railway.app/button.svg) Updated 3.84.1 - 13\. May 2026 ](https://railway.app/deploy/L8TUlT?referralCode=-Yg50p&ref=funkyton.com) ![Payload CMS railway template overview - click "Deploy Now"](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/payload-cms-v3-Railway-template-overview.png) Payload CMS railway template overview - click "Deploy Now" On the template overview page, you can see what this relatively simple template consists of: a PostgreSQL database and a Payload CMS Git repository, both preconfigured to work out of the box as a website/blog CMS. ![Click "Deploy"](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/deploy-payload-cms-v3-directly-on-railway.png) Click "Deploy" Ignore the two "configure" buttons, this template is preconfigured. Customizations will be covered later and cannot be done at this point in time. Click "Deploy" ![Locate your public URL](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/payload-successfully-deployed-on-Railway--how-to-access-your-CMS-dashboard.png) Locate your public URL Once you have two green checkmarks, select the payload-CMS service, and click the public URL in the deployments tab to visit your own Payload CMS for the first time. ![Payload CMS welcome screen](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/payloac-CMS-welcome-screen-visit-admin-dashboard.png) Payload CMS welcome screen If you've reached this screen, congratulations, everything went smoothly! From here, you'll find on-screen instructions to help you get started, but let me break it down quickly. First, you'll need to create an Admin user. Once logged in as the admin, the next step is to seed the database with some demo content. ![Click "Seed your database", wait for "Done" message, then click "visit your website"](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/payload-CMS-V3-admin-panel-welcome-screen-seed-database-and-visit-website.png) Click "Seed your database", wait for "Done" message, then click "visit your website" I've included demo data based on blog posts from this very blog, so you'll have some template posts to reference as you write your own pages or articles. ![Website view, these are demo post from seed data](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Payload-CMS-3-demo-data-blog-posts.png) Website view, these are demo post from seed data To edit any piece of content from your admin dashboard, click on a content category, such as Posts. Then, pick one of the demo posts to see how the editor works. It is a pretty powerful WYSIWYG editor and yet very intuitive. ![Payload CMS V3 content editor WYSIWYG](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Payload-CMS-V3-content-WYSIWYG-editor.png) Payload CMS V3 content editor WYSIWYG ### Customization and Integration Already eager to start customizing your website? We've got you covered, read and watch how to customize your project by following our guide below. [How to Add Comment Section to your Pages with Payload CMS 3Learn how to integrate a comment system into your Payload CMS v3 project with React. This guide offers 4 simple steps for creating a server-rendered Comment List and a client-side Comment Form, making it easy to boost user interaction in your web app. Suitable for developers at any level!![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/blog-cover-payload-cms-3-comment-system.png)](https://funkyton.com/payload-comment-section/) ### Medusa 2.0 on Railway: One-Click Deploy Template & Setup Guide (2026) URL: https://funkyton.com/medusajs-2-0-is-finally-here/ Last updated: 2026-08-31T19:10:03.000Z As of 23rd October, the medusa core team release the official 2.0.0 as promised! This means we are no longer working with a "preview" or "release candidate" version. In this article I will cover what's new in the next generation of medusa, and of course, share my one-click deploy template for Railway, which have been updated to the new final release version. *To skip reading and go straight to the deploy template, use the wheel thingy on your mouse, until you see a purple button.* ### Video instructions ### So what is new in MedusaJS 2.0? MedusaJS 2.0 introduces several exciting updates that enhance both the user experience and the developer's ability to customize the platform. - **Reworked Admin Dashboard**: The admin dashboard has been revamped to offer a more intuitive and aesthetically pleasing interface. This update simplifies tasks such as creating product variants, reducing the repetitive configuration needed when adding products with multiple options, like size and color. But also adds UI for additional features, such as...: - **Enhanced Inventory Management**: Inventory management now supports multiple warehouses, making it easier to scale your business without hassle. MedusaJS 2.0 ensures you have the tools needed for much more advanced inventory management compared to version 1.0. - **New Promotions Engine**: The updated promotions engine allows for detailed configuration of discounts and price campaigns, enhancing the admin/manager user experience by providing more flexibility in marketing strategies. **Under the hood, key changes;** - **Improved Software Architecture**: The architecture has been redesigned to facilitate greater customization, making it easier for developers to add new features to their e-commerce projects. This aligns with Medusa's goal of being a highly customizable e-commerce stack. From my experience, building custom features on this platform is intuitive and easy—just build, plug, and play. - **Database Structure Updates**: Changes to the database's relational structure enable more adaptability in the Medusa data model. You can now modify parts of the datamodel while excluding entities you don't need, further enhancing customization options. - **New Query Tool**: A new query tool simplifies data fetching from the database without needing to manually pass a database connection. This feature improves development speed and ease, as it handles database interactions seamlessly from any part of your application. It's a significant quality-of-life improvement for developers. For those using MedusaJS for a simple webshop with the storefront starter, there may not be many visible changes when upgrading to version 2.0 at this time. However, I still recommend using Medusa 2.0 for new projects since it's the latest major version that will continue to receive updates and new features. Overall, MedusaJS 2.0 offers substantial improvements that benefit both business operations and development processes. Whether you're managing inventory or building custom features, this update provides tools that make your e-commerce journey smoother and more efficient. At this point, open-source e-commerce platforms like MedusaJS are becoming serious competitors to enterprise solutions such as Shopify. If I were one of those enterprise competitors, I would definitely be on my toes, as MedusaJS continues to push the boundaries. You can read the full 2.0.0 change log here: [https://medusajs.com/changelog/#v2.0.0](https://medusajs.com/changelog/?ref=funkyton.com#v2.0.0) ### MedusaJS 2.0 Railway one-click deploy template *This is an independent, community-maintained template, not an official MedusaJS, Inc. product. It uses the official open-source Medusa 2.0 release as published, with no fork or patches to the core, and adds the Railway deployment setup plus preconfigured search, payment, email and file storage integrations on top. The official Medusa docs (*[*https://docs.medusajs.com/*](https://docs.medusajs.com/?ref=funkyton.com)*) apply as normal.* **Template includes:** - Storefront - Classic webshop *(open source)* - MedusaJS 2.x backend *(open source)* - MeiliSearch product search engine *(open source)* - Postgres database *(open source)* - Redis *(open source)* - Railway storage bucket for media (S3-compatible) ![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/medusajs-2-on-railway-services-overview-png.png?_a=BAMAPqkS0) As this is now the 4th Railway deploy template I am working on, I have gained some experience, which has resulted in this template being the best so far. Here are some improvements: - No setup needed. Storefront, Redis, Postgres database, Medusa 2.0 backend, storage service, search engine, automatically configured and connected. - Automatic sharing of publishable api key between backend and storefront. - Native Railway storage bucket (S3-compatible) for all your media, served through a tiny built-in proxy function. The template uses Medusa's stock S3 file provider, so you can point it at any S3-compatible storage (AWS S3, Cloudflare R2, ...) with a few environment variables. - Health checks for deploys. Green checkmark now means that your app is 100% ready and running, you don't need to wait another ½-1 minute. - Automatic admin user creation with a unique strong random password. - Automatic strong secrets for `JWT_SECRET` and `COOKIE_SECRET` ℹ️ **Previous versions of this template used MinIO; since MinIO's community edition entered maintenance mode, the template now uses Railway's native buckets instead.* Preconfigured services (optional): - Payment with Stripe, plugin preinstalled (api key needed) - Emailing with Resend or Sendgrid, plugin preinstalled (api key needed) ### Requirements - [Github account](https://github.com/?ref=funkyton.com) - [Railway account](https://railway.app/?referralCode=-Yg50p&ref=funkyton.com) (connected with GitHub account - important!) - Railway hobby plan (5$/m - if you want online hosting) [ ![Deploy on Railway](https://railway.app/button.svg) v2.19.0 🤩 updated 31\. August 2026 ](https://railway.com/deploy/medusajs-2-0-storefront?referralCode=-Yg50p&ref=funkyton.com) OBS: Hobby plan (5$/month) is required to run this template in the cloud💸- but if you don't want to pay, and just want to run it locally, [watch this video](https://youtu.be/PPxenu7IjGM?si=kvHNZyLd0VVndZ4I&ref=funkyton.com) and launch the trial template instead ⬇️ [Deploy & Host Medusajs 2.0 + Storefront (TRIAL) | RailwayDeploy and host Medusajs 2.0 + Storefront (TRIAL) on Railway in one click — always-on servers, built-in databases, and GitHub auto-deploys. Full ecommerce solution, manage products, inventory, orders, etc.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)RailwayRasmus Puls’s Projects![](https://og.railway.com/api/image?fileType=png&layoutName=template&TemplateImage=https%3A%2F%2Fres.cloudinary.com%2Fhczpmiapo%2Fimage%2Fupload%2Fv1788181218%2Ffunkyton-mark-round-256_chhpfb.png&Title=Medusajs+2.0+%2B+Storefront+%28TRIAL%29&Description=Full+ecommerce+solution%2C+manage+products%2C+inventory%2C+orders%2C+etc.&AuthorImage=https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F14924038%3Fv%3D4&AuthorName=Rasmus+Puls%27s+Projects&Category=Other)](https://railway.com/deploy/Fb6KUX?referralCode=-Yg50p&ref=funkyton.com) Updated on 31\. Aug. 2026 *Railway only allows free users a total of 3 services at once. The trial template does not contain the storage bucket, and uses non-production ready internal storage.* ### Looking for Medusa B2B version? After receiving a lot of feedback on the main Medusa v2 template and several requests for help setting up the official B2B starter on Railway, I decided to create a simple **B2B version** for anyone who wants to test it out. This one is less tested, less maintained, and has fewer built-in integrations. It's a straightforward [fork of the official Medusa B2B repo](https://github.com/medusajs/b2b-starter-medusa?ref=funkyton.com), adjusted to deploy seamlessly on Railway. [ ![Deploy on Railway](https://railway.app/button.svg) ⚠️ OBS: B2B version 2.17.2 Updated on July 23\. 2026 ](https://railway.com/deploy/medusajs-20-storefront-b2b?referralCode=-Yg50p&ref=funkyton.com) ### Successfully deployed, what's next? Congratulations! You've successfully deployed the seed of your project - a somewhat generic, yet fully functional Medusa.js 2.0 stack. While it is not personalized yet, it's a robust foundation for you to build on top of. Now comes the fun part: growing, shaping, and maintaining your project to align with your vision, whether it's for a hobby project or a business venture. In the deployment video, I demonstrated a simple customization to get you started. However, to help you take your project to the next level, I am actively maintaining a list of invaluable resources. ### Self help - GitHub repository: [https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate](https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate?ref=funkyton.com) - Medusajs 2.0 official documentation: [https://docs.medusajs.com/](https://docs.medusajs.com/?ref=funkyton.com) - Medusajs release overview: [https://medusajs.com/changelog/](https://medusajs.com/changelog/?ref=funkyton.com) - Medusajs 2.0 useful examples: [https://github.com/medusajs/examples](https://github.com/medusajs/examples?ref=funkyton.com) - How to setup "Server & Worker" architecture: [https://youtu.be/llFFn2Eu6is](https://youtu.be/llFFn2Eu6is?ref=funkyton.com) - Solve Slow Railway app performance: [https://youtu.be/z6MEZqSwRlM](https://youtu.be/z6MEZqSwRlM?ref=funkyton.com) [How to Run your Medusajs 2.0 boilerplate locallyRequirements Until now, everything has been done in your browser, but that is about to change. To make changes to your medusa project, you will need some web development tools. If you already have these tools, please scroll past this step. If you have never heard of any of these![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/run-medusa-2.0-mono-repo-locally-blog-cover.jpg)](https://funkyton.com/how-to-run-your-medusajs-2-0-boilerplate-locally/) [How to add Stripe payment module to medusajs 2.0Video Instructions Links Stripe module medusa 2.0 documentation: https://docs.medusajs.com/v2/resources/commerce-modules/payment/payment-provider/stripe GitHub repo config example: https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate/blob/master/backend/medusa-config.js Video demonstarti…![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/blog-cover-medusa-2.0-stripe-integration.jpg)](https://funkyton.com/add-stripe-payment-module-to-medusajs-2-0/) [How to Add MeiliSearch to Your Medusa 2.0 ProjectEnhance your Medusa 2.0 project with powerful search functionality using MeiliSearch. This guide walks you through setting up a MeiliSearch instance, configuring the backend with a community plugin, and integrating it into your storefront for seamless product search.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/MeiliSearch-medusajs-integation-tutorial-cover-image.png)](https://funkyton.com/how-to-add-meilisearch-to-your-medusa-2-0-project-2/) - How to update dependencies: [https://youtu.be/t-L9nxBloBY?si=vL\_Np76PPgmog\_iA](https://youtu.be/t-L9nxBloBY?si=vL%5FNp76PPgmog%5FiA&ref=funkyton.com) - Automated email (part 1) - Setup Resend: [https://youtu.be/pbdZm26YDpE](https://youtu.be/pbdZm26YDpE?ref=funkyton.com) - Automated email (part 2) - Tutorial, send automated email based on ANY kind of backend event: [https://youtu.be/38tEhuZTrdY](https://youtu.be/38tEhuZTrdY?ref=funkyton.com) - Alex explains how to add authentication with Kinde: [https://www.youtube.com/watch?v=HC6Gsba8dCY](https://www.youtube.com/watch?v=HC6Gsba8dCY&ref=funkyton.com) and follow-up: [https://www.youtube.com/watch?v=xkb-XzPAKKE](https://www.youtube.com/watch?v=xkb-XzPAKKE&ref=funkyton.com) - How to fix broken Medusa database: [https://youtu.be/6hKahRqL9YQ](https://youtu.be/6hKahRqL9YQ?ref=funkyton.com) - Adil explains Server Side cache. How to, and why: [Caching - perseidesImplementing caching in Medusa![](https://static.ghost.org/v5.0.0/images/link-icon.svg)perseides![](https://perseides.mintlify.app/mintlify-assets/_next/image?url=%2Fapi%2Fog%3Fdivision%3DDocumentation%26mode%3Dlight%26title%3DCaching%26description%3DImplementing%2Bcaching%2Bin%2BMedusa%26logoLight%3Dhttps%253A%252F%252Fmintlify.s3.us-west-1.amazonaws.com%252Fperseides%252Flogo%252Flight.svg%26logoDark%3Dhttps%253A%252F%252Fmintlify.s3.us-west-1.amazonaws.com%252Fperseides%252Flogo%252Fdark.svg%26primaryColor%3D%25230c0a09%26lightColor%3D%2523e5e5e5%26darkColor%3D%25230c0a09&w=1200&q=100)](https://docs.perseides.org/guides/v2/caching?ref=funkyton.com) Solve media upload error: [https://youtu.be/ToY9ge2vS80](https://youtu.be/ToY9ge2vS80?ref=funkyton.com) (only relevant for **older** versions of this template) ... more coming soon! ### Ask for help - Medusajs community: [https://discord.com/invite/medusajs](https://discord.com/invite/medusajs?ref=funkyton.com) - Deploy template related discussion: [https://help.railway.app/discuss/thread/medusajs-2-0-storefront-c69ac579](https://help.railway.app/discuss/thread/medusajs-2-0-storefront-c69ac579?ref=funkyton.com) - Railway community: [https://discord.com/invite/railway](https://discord.com/invite/railway?ref=funkyton.com) ### Special thanks to Thanks to everyone who directly or indirectly contributed to this supercharged Medusa 2.0 starter template! - [https://github.com/aleciavoge](https://github.com/aleciavogel?ref=funkyton.com) ([http://alecia.ca/](http://alecia.ca/?ref=funkyton.com)) Helped with pnpm setup, Resend notification improvements and more QoL dev improvements. - [https://github.com/rokmohar](https://github.com/rokmohar?ref=funkyton.com) ([http://www.mohar.xyz/](http://www.mohar.xyz/?ref=funkyton.com)) - Developer of the MeiliSearch module used in this template! ### Analytics disclaimer I have decided to introduce a little piece of analytics in this template. This is turned on by default, but not required, and can be turned off by deleting the environment variable on the backend service with the key: `TEMPLATE_REPORTER_URL`. Normally, I can see in the Railway dashboard how many people have used my deploy template, and how many live projects there are - but I recently discovered that those numbers are incorrect, and therefore, I was quite devastated when I found out that one of my templates, which I was investing a lot of time in, was barely used by anyone. For that reason, I have decided to take user engagement analytics into my own hands, and I really hope that you will participate and leave the feature on to help me get a better idea where my time is best spent. When left on, I will be pinged with your railway project ID when you launch the template - no personal information is tracked. Also... I welcome questions from people, and I'm happy to help as much as I can. However, it can be time-consuming. Not so long ago, I had a bad experience with someone who cloned my Git repository, made changes, and attempted to deploy it on Railway. When this didn't work out for them, they reached out to me, pretending there was a problem with my deployment template and asked for help in debugging their error messages. At some point, I realized what was happening and told them to use the template if they wanted it to work out of the box. If they wanted something different, they would need to spend their own time making that work. This is another reason why I'd like to track project IDs when my template is used, so I can verify that someone asking for help is actually a template user. So, if you reach out with a question, I might ask for your project ID, something for something 😉 ### Medusajs 2.0-rc (release candidate) - We are getting close! URL: https://funkyton.com/medusajs-2-0-rc-release-candidate-we-are-getting-close/ Last updated: 2026-08-29T20:38:07.000Z The release candidate version of 2.0 has now been available for testing for a few weeks, although the Medusa core team has announced an "official release" on October 23rd, Are they talking about an alpha version? I'm not really sure yet, but we will find out soon! You can read their full [announcement blog post here](https://medusajs.com/blog/v2-rc/?ref=funkyton.com). In the mean time, let's take a look at the current release candidate version. I have updated the Railway template, to the new `rc` version. And, thank you to everyone who provided feedback, I have tried to listen and fix a number of issues that you have pointed out. ### Improvements to Deploy Template - Customer signup - fixed - Image/media upload - fixed - Automated API key generation and setup - added - Simplified stack - Using [medusajs-launch-utils](https://www.npmjs.com/package/medusajs-launch-utils?ref=funkyton.com) ### Launch your own 2.0-rc [ ![Deploy on Railway](https://railway.app/button.svg) ](https://railway.com/deploy/medusajs-2-0-storefront?referralCode=-Yg50p&ref=funkyton.com) ### Template Demo video ## Deploy Template Roadmap I am always trying to improve the template, and based on your feedback, these are the topics I will be looking into: - MeiliSearch implementation - Automated email sending - Production ready architecture (Server + Worker) - Adding CMS to storefront ### How to Run your Medusajs 2.0 boilerplate locally URL: https://funkyton.com/how-to-run-your-medusajs-2-0-boilerplate-locally/ Last updated: 2026-07-21T16:32:38.000Z ## Requirements Until now, everything has been done in your browser, but that is about to change. To make changes to your medusa project, you will need some web development tools. If you already have these tools, please scroll past this step. If you have never heard of any of these tools, you're in for a challenge, as this will get quite technical. - **Medusajs 2.0 boilerplate project:** If you didn't already launch the one-click deploy template, you will need to do that first, it takes about 10 minutes, instructions can be found here: - **Git**: A command-line tool necessary for pushing your updates to the build pipeline. You can download Git from [Git's official website](https://git-scm.com/downloads?ref=funkyton.com). Choose the version compatible with your operating system and follow the installation instructions. - **Visual Studio Code (VS Code)**: VS Code is a lightweight, yet powerful and free code editor developed by Microsoft. Download it from the [official Visual Studio Code website](https://code.visualstudio.com/?ref=funkyton.com). Select the version for your operating system. - **Node.js**: Node.js is a runtime environment that allows you to run JavaScript on the server side. It's required to execute the JavaScript code you'll be working with. Visit the [official Node.js website](https://nodejs.org/?ref=funkyton.com) to download. It is recommended to download the LTS (Long Term Support) version for better stability. ### Video Instructions ### Setup In case you don't already have the `.env.local` template file in your /storefront, you may copy the contents here: ``` NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000 NEXT_PUBLIC_BASE_URL=http://localhost:8000 ``` ## NEW! Docker Compose support (July 2026) The boilerplate now ships with a `docker-compose.yml` in the repository root that spins up the same services you get on Railway; Postgres, Redis, MeiliSearch, and an S3-compatible storage bucket (MinIO) so you can run the full stack locally with one command: ```bash docker compose up -d ``` That's it. A small init job even creates the `medusa-media` bucket for you and makes it publicly readable, so product images work locally out of the box. To connect your backend to these services, open `backend/.env` and uncomment the `S3_*` and `MEILISEARCH_*` values in `.env.template` they already match the docker-compose setup exactly. The default `DATABASE_URL` and `REDIS_URL` values match too. Then run the backend and storefront as usual: ```bash cd backend && pnpm ib && pnpm dev # backend on localhost:9000 cd storefront && pnpm dev # storefront on localhost:8000 ``` A few handy URLs while developing: - **MeiliSearch dashboard:** [http://localhost:7700](http://localhost:7700/?ref=funkyton.com) (key: `local_meilisearch_master_key`) - **MinIO console:** [http://localhost:9005](http://localhost:9005/?ref=funkyton.com) (login: `medusa` / `supersecret`) Everything runs in named volumes, so your data survives restarts. `docker compose stop` pauses the stack, `docker compose down -v` wipes it for a fresh start. Before this, testing things like search and file uploads locally meant connecting to your cloud services or setting each one up by hand, now the whole Railway-like environment lives on your machine. 🎉 ### How to add Stripe payment module to Medusajs 2.0 URL: https://funkyton.com/add-stripe-payment-module-to-medusajs-2-0/ Last updated: 2025-12-25T12:19:45.000Z ### Video Instructions ## **Prereq**ui**sites** Before starting, ensure you have: - A deployed MedusaJS 2.0 stack (e.g., using [the Railway deploy template](https://railway.com/deploy/gkU-27?referralCode=-Yg50p&ref=funkyton.com)). - A Stripe account with Test Mode enabled for testing. - Basic familiarity with MedusaJS and its configuration. If you haven't set up your Medusa project yet, you can use the[ one-click deploy template](https://funkyton.com/medusajs-2-0-is-finally-here/#requirements) for a quick start. ## **For Custom** Projec**ts** If you're ***not*** using the pre-configured Railway template: 1. Install the Stripe payment module in your Medusa backend: ```bash npm install @medusajs/payments-stripe@latest ``` 1. Add the following configuration to your `medusa-config.js` file: ```javascript const medusaConfig = { // Your other config modules: [ // Your other modules here { key: Modules.PAYMENT, resolve: '@medusajs/payment', options: { providers: [ { resolve: '@medusajs/payment-stripe', id: 'stripe', options: { apiKey: STRIPE_API_KEY, webhookSecret: STRIPE_WEBHOOK_SECRET, }, }, ], }, } ], }; ``` ## **For R**ailway **Deploy Template Users** The Stripe module is already installed and pre-configured in the `medusa-config.js` file. You only need to set up environment variables. ## **Step 2:** Set Up **Environment Variables** Add the following environment variables to your backend and storefront projects: ### **Backend Variables** - `STRIPE_API_KEY`: Your Stripe secret API key. - `STRIPE_WEBHOOK_SECRET`: The webhook secret generated in Stripe. ### **Storefront Variable** - `NEXT_PUBLIC_STRIPE_KEY`: Your Stripe publishable API key (found in the Stripe dashboard). For Railway users: 1. Navigate to your project in Railway. 2. Add these variables under the "Variables" section for both backend and storefront services. ## **Step 3: Create a Webhook in Stripe** 1. Go to your [Stripe dashboard](https://dashboard.stripe.com/?ref=funkyton.com). 2. Navigate to **Developers > Webhooks** and click "Add endpoint." 3. Use the following URL format for your webhook endpoint: `https:///hooks/payment/stripe_stripe` 4. Add the required payment events: - `payment_intent.succeeded` - `payment_intent.amount_capturable_updated` - `payment_intent.payment_failed` 5. Save the webhook and copy the generated webhook secret. 6. Add this secret as the `STRIPE_WEBHOOK_SECRET` environment variable in your backend. ## **Step 4**: Configure Payment Providers in Admin Dash**board** 1. Log in to your Medusa Admin Dashboard. 2. Go to **Settings > Regions**. 3. Edit your region(s) and add "Stripe" as a payment provider. 4. Save changes. ## **Step 5: Test Y**our Integr**ation** 1. Enable Test Mode in your Stripe dashboard. 2. Use the test credit card number `4242 4242 4242 4242` with any valid expiration date, CVC, and postal code during checkout. 3. Verify that transactions are processed successfully in both the storefront and Stripe dashboard. ## **Step 6: Switch** to Production When ready for production: 1. Disable Test Mode in Stripe. 2. Replace test API keys with live API keys in your environment variables. 3. Update webhooks if necessary. ## How to sign up with Stripe If you are having difficulties navigating the Stripe dashboard or need help with the signup process, I have covered this in great detail in another article where I was setting up Stripe for a Vendure webshop. You can refer to all the steps related to finding API keys, and creating webhooks, etc., from that guide. Visit the article below, and scroll to "Step 1: Stripe setup". [Tutorial: Add Stripe credit card payment to VendureThis tutorial guides you through integrating Stripe payments into your Vendure webshop, enabling credit card and Google Pay options for customers. Follow the step-by-step process to set up Stripe, configure webhooks, and update your Railway deployment for a seamless checkout experience.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-140953.png)](https://funkyton.com/add-stripe-payment-to-vendure/) ### Useful Links - Stripe module medusa 2.0 documentation: [https://docs.medusajs.com/v2/resources/commerce-modules/payment/payment-provider/stripe](https://docs.medusajs.com/v2/resources/commerce-modules/payment/payment-provider/stripe?ref=funkyton.com) - Alternative Storefront Setup instructions: [https://docs.medusajs.com/resources/storefront-development/checkout/payment/stripe](https://docs.medusajs.com/resources/storefront-development/checkout/payment/stripe?ref=funkyton.com) - GitHub repo config example: [https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate/blob/master/backend/medusa-config.js](https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate/blob/master/backend/medusa-config.js?ref=funkyton.com) - Video demonstration of signing up for a Stripe account **(watch from 01:30 - 06:30):** [https://www.youtube.com/watch?v=gqX4j9Ms7Lo&t=98s](https://www.youtube.com/watch?v=gqX4j9Ms7Lo&t=98s&ref=funkyton.com) ## For all resources about MedusaJS [Medusa.js Tutorials, Deployment Guides & Templates | FunkytonExplore Medusa.js tutorials, pros and cons, and deployment guides. Launch scalable e-commerce projects with ease using our starter templates at Funkyton.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)Funkyton![](https://images.unsplash.com/photo-1592503254549-d83d24a4dfab?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fGVjb21tZXJjZXxlbnwwfHx8fDE3MzgwOTcwNTV8MA&ixlib=rb-4.0.3&q=80&w=2000)](https://funkyton.com/tag/medusa/) ### Open source basboard clip system URL: https://funkyton.com/open-source-basboard-clip-system/ Last updated: 2025-01-29T15:27:46.000Z Discover a versatile open-source clip system for mounting baseboards on walls or furniture legs. This 3D-printable solution includes wall clips, custom leg clamps for kitchen cabinets, and a dedicated saw box for precise cuts. Perfect for DIY enthusiasts, with free design files included! _This post is for subscribers only._ ### Quadlock connector hidden inside steerer tube (custom 3D print) URL: https://funkyton.com/quadlock-pipe-mount/ Last updated: 2025-01-29T15:25:24.000Z I'm doing a complete makeover of my ADO Air 20 folding electric bike, and I have very high requirements for the aesthetic result. I've never liked the look of bicycle accessories that clamp around the handlebar with bolts and nuts sticking out, or for that matter, those strapped with zip ties or tightening bands. For an elegant solution to add a Quadlock mount for my phone to my bicycle, I decided to design an adapter that hides inside the head tube. To secure it, I am using a tightening screw that, when tightened, expands the diameter of the tube insert, locking it in place—and it worked out absolutely flawlessly! Link to download: [https://www.thingiverse.com/thing:6671872](https://www.thingiverse.com/thing:6671872?ref=funkyton.com) ### Result ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1950.jpg) ![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1953.jpg) ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1951.jpg) ![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1952.jpg) ### Tension mechanics When the long M4 screw is tightened, the wedge gets pulled into the main piece, forcing the sides outwards. This creates very strong friction on the sides of the tube, securing the part. ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1946.jpg) ![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1945.jpg) ### Print settings (slicing and support) To print this design, you need to add two pauses for inserting an M4 nut: one for the angled piece to screw on the official Quadlock mount, and one inside the wedge-shaped end piece. For support, you also need to add at least one support blocker in the center of the main piece to ensure that the nut can be inserted during the print pause. Please see the pictures below for a visual explanation. ![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/quad-lock-crow-tube-adapter-support-print-settings.png) ![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1941.jpg) ![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/quad-lock-crow-tube-adapter-pause-for-insert.png) ![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/quad-lock-crow-tube-adapter-sliced.png) Slicing guidelines ### Assembly I did not design the Quadlock locking mechanism itself, as I wanted an injection-molded, strong piece for this. I do not trust that a 3D-printed part will be strong and durable enough for this purpose, as the parts are thin and delicate and receive quite some stress when riding on rough terrain. You can find [the pieces needed here](https://www.quadlockcase.eu/collections/shop-mounts/products/bike-mount?variant=262854443024&ref=funkyton.com). ![Parts and assemply overview](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_1944.jpg) **License notice:** I have been in contact with Quadlock regarding sharing a design that fits with their patented system - I thank them very much for being cool about it and allowing DIY communities to build on top of their platform! However, they made it very clear that selling these parts is against their policy, which is totally fair in my opinion. Therefore, please be aware of the license of the file, CC Non-Commercial: [https://creativecommons.org/licenses/by-nc/4.0/deed.en](https://creativecommons.org/licenses/by-nc/4.0/deed.en?ref=funkyton.com) Please be respectful about that. 🙏 ### Tutorial: Add Stripe credit card payment to Vendure URL: https://funkyton.com/add-stripe-payment-to-vendure/ Last updated: 2025-01-29T15:23:30.000Z This article is a short and easy tutorial that will guide you through integrating Stripe payment in your Vendure application, so that customers will be able to purchase your items on your webshop using a credit card or Google Pay. So stay tuned! ### Prerequisite You must have a running Vendure backend and webshop, like the one we deployed on Railway in the last tutorial in less than 10 minutes! If you do not already have a hosted Vendure webshop, please complete the one-click deploy tutorial first: [Vendure commerce platform, review and tutorial - yet another open souce shopify killer?This week we are taking a look at a really interesting open source commerce project called Vendure. I was made aware of Vendure while researching medusajs (another similar project). And since I had some hickups with the current latest version of Medusa, and are eagerly waiting for Medusa 2.0![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/vendure-cover.png)](https://funkyton.com/vendure-tutorial/) Additionally, you will need a Stripe user. Sign up at: [Stripe Registration](https://dashboard.stripe.com/register?ref=funkyton.com) ### Step 1: Stripe setup Once you have a Stripe user, you need to add your webshop. In the top left corner, click "Create new account": ![Create Account in Stripe](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-114245.png) ![How to setup your account in Stripe](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-114448.png) Fill in your details (company or personal), then proceed to "Business details." It is important that you use your correct Vendure webshop (storefront) public URL in this field. As soon as you paste it in, Stripe performs some checks to see that the website actually exists and runs a little analysis. As for the product description, this field is also important. They might reject your application if you do not provide a sufficient description or if your described product is against their ToS. If you have not set up a custom domain on Railway yet but are running on your Railway-provided domain, you can use that. You can find it in your Railway project dashboard by selecting your Storefront service and clicking on the "deployments" tab, then copy the URL in the green deployment box: ![How to find storefront public URL needed for Stripe](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-115835.png) Paste the storefront URL in Stripe "business details." ![Add storefront URL to Stripe, and fill in description](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-120159.png) If you have set up a custom domain, use that instead. The rest of the setup is straightforward. Just fill in all the required fields and finalize your application. Then you will be taken to your dashboard. From here, we need to find your API keys. Navigate to the "developers" section: ![Locate the Developers menu](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-121203.png) ![Locate the API key section](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-121352.png) You are looking for these two keys. Keep this tab open so you have it ready for later. Then open "webhooks" in a new tab (right next to API keys) and add a new endpoint: ![How to add webhook endoint in Stripe dashboard](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-122329.png) Keep this tab open as well, and open a third new browser tab. In this tab, head over to your Railway project dashboard and select your backend service. You need to copy the backend URL, just like you copied the storefront URL earlier: ![How to find your backend URL needed for Stripe webhook](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-122537.png) Once you have your backend public URL on the clipboard, go back to the webhooks tab and paste in the URL, then add `/payments/stripe` to the end of the URL in the field, and then click on the "+ select events" button. ![How to configure webhook for Vendure in Stripe](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-122742.png) Here is the list of events that you need to add: - `payment_intent.succeeded` - `payment_intent.payment_failed` - `invoice.payment_succeeded` - `invoice.payment_failed` - `checkout.session.completed` - `customer.subscription.created` - `customer.subscription.updated` - `customer.subscription.deleted` - `invoice.created` - `invoice.finalized` ![How to add all required Stripe events to webhook for Vendure](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-123927.png) Click "Add events," then click "Add endpoint." You should now have your webhook. Keep this tab open, as you need the `signing secret` close at hand. ![How to find your Stripe webhook signing key](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-125048.png) ### Step 2: Setup in Vendure Admin Dashboard Go to your admin dashboard:`/admin/` and expand the "settings" section in the menu, then click on "payment methods": ![How to enable Stripe as payment method in Vendure after setup](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-124434.png) Fill in the name of the payment method. I call mine "stripe." Then expand the payment handler button at the bottom and choose "stripe payments," which is available because I installed the Stripe payment plugin when I created the Railway deploy template. If you are following this guide and trying to add Stripe to a Vendure app created in a different way, you will need to install the Stripe payment plugin yourself. In the Stripe payment handler, you need to provide the webhook signing secret and API key from the two tabs we kept open. Copy the API **secret key** into the API key field in Vendure. ![How to find your Stripe secret API key needed for Vendure](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-125450.png) And from the webhooks tab, copy the webhook **signing secret** into the corresponding field. ![How to find Webhook Signing secret in Stripe](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-125048.png) How to find Webhook Signing secret in Stripe Then click "create" to finalize. ![How to setup Stripe API key values in Vendure to enable payment method](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-131859.png) One final thing that I like to do is removing the standard payment method once Stripe has been added. This ensures that customers will check out with a real payment. ![How to delete the manual payment method after enabling Stripe](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-134918.png) ### Step 3: Setup in Railway You are almost there. We just need to provide the storefront with a publishable API key. Open a new tab and head over to the Railway project dashboard. Select your storefront service and open the tab "environment variables." Here you want to add a new entry. The key is: `STRIPE_PUBLISHABLE_KEY` and the value is your publishable API key from Stripe (I hope you kept that tab open). ⬇️ ![How to find Stripe API key needed for Railway](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-135220.png) Copy this key and paste it in here: ![How to add Stripe API key for storefront in Railway](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-135533.png) Click "add," and a change will be found, which must be deployed. Click "deploy now" and wait for the deployment to complete. Once the deployment has finished (about 2 minutes), you are done! 🎉 ### Step 4: Verify Success Visit your webshop (storefront) and add an item to your basket. Go to the checkout page and fill in your name, address, and choose a shipping method. Then proceed to payment, where you should now see the Stripe credit card and Google Pay form. ![Checkout page after setup, showing dard and google pay form](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-140953.png) ### Vendure Commerce Platform, Review and Tutorial - Yet Another Open Source Shopify Killer? URL: https://funkyton.com/vendure-tutorial/ Last updated: 2026-02-26T13:45:19.000Z This week we are taking a look at a really interesting open source commerce project called Vendure. I was made aware of Vendure while researching MedusaJS (another similar project). And since I had some hickups with the current latest version of Medusa, and are eagerly waiting for Medusa 2.0 to be launched, I thought I would give Vendure a shot, and see if it would do better - and it kinda did! On some key areas at least. 💡 ****Updated to Vendure 3.5.0** (12th Nov. 2025) - the latest stable release with improved performance and new features. Now optimized for Railway's Railpack deployment system and using PNPM for faster, more reliable package management. If you already know enough about Vendure, and just want to get to the juice, scroll down to the [**Tutorial section**](https://funkyton.com/vendure-tutorial/#the-tutorial), to find the automated deploy template. ### What is Vendure and what can you expect from it? This is what Vendure says: > Don't let your commerce platform define the limits of your ambitions. > > Vendure provides the solid foundation for any commerce application: inventory management, orders, customers, taxes, promotions and more. > > Our battle-tested, stable core powers start-ups, direct-to-consumer brands, Fortune 500 enterprises and everything in between. > > Vendure's cutting-edge tech stack and extensibility future-proofs your digital commerce, reduces the risk of replatforming. > > Our laser focus on developer experience means faster time-to-market, fewer bugs, and happy developers who can focus on shipping features rather than battling platform limitations. [https://vendure.io/](https://vendure.io/?ref=funkyton.com) ### Vendure vs. Medusajs This comparison was made ****before** the launch of [Medusa 2.0](https://funkyton.com/medusajs-2-0-is-finally-here/). The new version of MedusaJS has several improvements. Since I recently played around with medusajs while building [the medusajs deploy template and writing a tutorial](https://funkyton.com/medusajs-free-fully-open-source-ecommerce-solution/), I know my way around it a little, and thought it would make sense to do a little comparison of the two. Here are the key differences between Vendure and medusajs that I have identified: **Product setup** is easier and faster in the Vendure admin dashboard compared to Medusa. Especially for products that exist in multiple variants, such as clothing that comes in sizes and colors. I added a t-shirt that comes in 6 sizes and 3 colors, which took about 2 minutes times 3 colors in Vendure. In Medusa, if filling in the minimum required product details, you can multiply those 2 minutes by 6 and then by 3 - and it is painfully boring, repetitive work. Vendure could use an improved way to assign images to multple variants at once, this is currently also very repetivie and prone to human error. **Learning curve of admin dashboard**: I'd have to declare Medusa the winner, but not by far. In my opinion, the user interface is slightly more intuitive; I don't remember ever having to search the internet for setup guidance or ask for help. However, in Vendure, I didn't manage to figure out how to add my products to a collection without asking in the Vendure Discord and was pointed to the right place in the documentation that described how to do so. Other than that, both are very intuitive and modern in the UX. **Setting up email templates:** With Vendure, the email templates live with the source code of the backend, so I manage them in my code editor, and changes have to be deployed. With Medusa, (the current only acceptable user experience) is via Sendgrid, so you have to sign up on a 3rd party website, connect your app, install a plugin, and then manage your email templates on Sendgrid's dashboard. I can see how the Sendgrid email template web editor is nice if you are not a techie, but then again, if you are not a techie, the setup process and linking your templates to your Medusa app will be a nightmare. For that reason, I declare Vendure the winner for managing email templates, but both could be better in this area. **Production readiness**: Both platforms are very far already and can be used for professional purposes already - with some tinkering. However, I find the Vendure experience a little more bug-free than I did with the Medusa experience. I ran into some issues with Medusa where my stock on certain product variants would not be updated no matter how many times I changed the inventory stock number and saved - in the webshop, they would still say "sold out". I also experienced other product detail fields suddenly missing in Medusa after having configured them, so I have a feeling that some overwrite is going on when saving products that are already live. At the time of writing this article, Vendure is clearly ahead of Medusa, but both platforms are rapidly releasing and listening to their users, so this section of the article could be incorrect down the line. In other words, if you are reading this section later than 3 months after this article is released, I wouldn't take it too seriously. **Longevity**: Vendure is just about to pass a 7-year Git birthday milestone, and the first alpha version was released in May 2019\. This is a significant amount of time to prove your existence as an open-source project of this magnitude. MedusaJS is younger, which is probably why it feels slightly less production-ready than Vendure. However, MedusaJS has built a larger user portfolio in a shorter time. They have four times as many GitHub stars (yes, I know... it's not the best KPI to measure), and Medusa is backed by a larger number of companies. This tells me that neither MedusaJS nor Vendure is likely to stop development anytime soon. I know that the Medusa team is working hard to release version 2.0 after the summer, so there are definitely updates ahead. Both projects have a roadmap if you are curious to see what's likely to arrive in the future; [Vendrure Roadmap](https://vendure-ecommerce.notion.site/bc46f41139b1472ca82e6bd7e7b2e223?v=91aab8121822495c98eac744fc359a74&ref=funkyton.com), [Medusajs Roadmap](https://medusajs.com/roadmap/?ref=funkyton.com). On aspects like; community, performance (server/hosting), tech stack, modularity, documentation and features - I found the difference to be minuscule, and both Medusa and Vendure are really impressive in those areas. The overall winner is going to be Vendure, especially for the product variant management and the so far bug-free UX. ### Live demo 😩 As of January 5, 2025, the demo webshop hosted on shop.funkyton.com has been discontinued. The decision was made to reduce hosting costs and focus resources on other projects. ## The Tutorial What is a headless platform without a head? Not much. But not to worry, I have compiled a deploy template that you are free to use, that deploys the vendure headless backend + admin dashbaord, asset storage, a storefron (webshop) - everything connected, no configuration needed! Additionally I have preinstalled Stripe payment and sendgrid email plugin, if you want to use either of these, you just need to connect your user accounts from those service, which is not part of this tutorial, but will be covered in a soon coming video tutorial. ### Overview Difficulty: Very easy - no tech skill required. Estimated time: 5-15 minutes Credit Card required: Yes, $5 Railway hobby plan ### Step 1: Sign up to GitHub and Railway If you don't already have a GitHub account, head over to [https://github.com/](https://github.com/signup?ref%5Fcta=Sign+up&ref%5Floc=header+logged+out&ref%5Fpage=%2F&source=header-home) and create a user. Once you have a GitHub account, you need to connect it to Railway. Head over to: [https://railway.app/](https://railway.app/?referralCode=-Yg50p&ref=funkyton.com) and sign in with GitHub ![Sign in with GitHub](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-182057.png) Once you are signed in, and have accepted Railways terms and conditions, you will find the page shown below. You need to upgrade to at least the "Hobby plan" in order to proceed with this tutorial. By the time of writing this tutorial, the hobby tier is $5/mo ![Choose Hobby Plan](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-174616.png) ### Step 2: Deploy vendure template on Railway This will look familiar to you if you have previously followed my [medusajs on railway tutorial](https://funkyton.com/medusajs-free-fully-open-source-ecommerce-solution/). But if not, you're in for an easy one this time. I managed to create the one-click deploy template for railway, so no configuration is needed what so ever, all the setup have been automated. You will find the template by clicking the button below: [ ![Deploy on Railway](https://railway.app/button.svg) Updated: Nov. 2025 - version 3.5.0 ](https://railway.app/deploy/6DeBLr?referralCode=-Yg50p&ref=funkyton.com) You will see the template overview, it should like something like this: ![Deploy template overview explained](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-125806.png) Because all required configuration is done in the template, you will see 3 green checkmarks straight away. However! I advice you strongly to click "configure" on the backend service, expand the 13 configured environment variables, and change the value of the three following: `COOKIE_SECRET`,`SUPERADMIN_PASSWORD`,`SUPERADMIN_USERNAME` to keep uninvited visitors out of your admin dashboard! Once that is done, you may click the deploy button ![Service overview before deploying](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-130631.png) ### Step 3: Wait for build to succeed and you are in business 🥳 Build should take about 3-5 minutes. Once you have a canvas with all green check marks you are good to go. ![Project canvas with deployed services](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-230842.png) To access your admin dashboard first select the "vendure-backend" service, then click on the url in the green deployment box: ![How to find your public URL for the admin dashboard](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-231046.png) If your result is a page with a railway logo and a message, something like: "the application failed to respond" or "there is nothing here yet" - you are just a little too early. Wait another minute or two and try again. Perform a hard refresh for good meassure.. Once your app is being serverd, you will see this screen: ![How to modify the URL to reach the admin dashboard route](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-231548.png) Add `/admin` to the URL and refresh, you should now see your admin dashboad: ![Your welcome and login screen to the admin dashboard](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-231731.png) Congratulations if you made it this far !🎈 You are now ready to login with the admin username and password (not "superadmin" because you totally changed that, as I adviced you to do, while configuring the deploy template in step 2 😉) To see your webshop (the storefront), go back to your railway project dashbord, select the "vendure-storefront" service, click the URL in the green deployment box: ![How to find your public URL to the storefront](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-232209.png) Your empty webshop should look like this: ![Preview of the webshop storefront](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-05-12-232411.png) ### Followup tutorial: [Tutorial: Add Stripe credit card payment to VendureThis tutorial guides you through integrating Stripe payments into your Vendure webshop, enabling credit card and Google Pay options for customers. Follow the step-by-step process to set up Stripe, configure webhooks, and update your Railway deployment for a seamless checkout experience.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-06-07-140953.png)](https://funkyton.com/add-stripe-payment-to-vendure/) ### Tutorial: Personalize your MedusaJS webshop (storefront) URL: https://funkyton.com/tutorial-customize-your-medusajs-webshop-storefron/ Last updated: 2026-04-23T23:04:58.000Z This tutorial is part II of the medusa on railway series, where we will be customizing your medusajs webshop! If you didn't already complete step I please do so, in order to proceed with this part of the tutorial. *OBS: Change at Railway! Please note that some information in the video is out of date - if you deployed your template later than 22th March. *Scroll down to "updated extra steps"*.* ### Video guide ### Link to part I [Tutorial: Medusajs on Railway - DIY ecommerce, has never been easier!By following this tutorial, you will have a fully operational ecommerce solution; webshop, order and pruduct management - up and running, on a server that you manage. Medusajs is an amazing open source project, that by the time of writing this tutorial, is mature enough to replace most of the![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-05-221337.png)](https://funkyton.com/medusajs-free-fully-open-source-ecommerce-solution/) ## Requirements Until now, everything has been done in your browser, but that is about to change. To make changes to your webshop, you will need some web development tools. If you already have these tools, please scroll past this step. If you have never heard of any of these tools, you're in for a challenge, as this will get quite technical. - **Git**: A command-line tool necessary for pushing your updates to the build pipeline. You can download Git from [Git's official website](https://git-scm.com/downloads?ref=funkyton.com). Choose the version compatible with your operating system and follow the installation instructions. - **Visual Studio Code (VS Code)**: VS Code is a lightweight, yet powerful and free code editor developed by Microsoft. Download it from the [official Visual Studio Code website](https://code.visualstudio.com/?ref=funkyton.com). Select the version for your operating system. - **Node.js**: Node.js is a runtime environment that allows you to run JavaScript on the server side. It's required to execute the JavaScript code you'll be working with. Visit the [official Node.js website](https://nodejs.org/?ref=funkyton.com) to download. It is recommended to download the LTS (Long Term Support) version for better stability. ## Updated extra steps to facilitate chages at railway. As mentioned above, there have been some changes at railway. When launching the template, you are no longer asked to name your own repository. Instead your railway project points directly to the upstream boilerplate repo. To make customizatinos to the code, you first have to "eject" from the source repo. This feature is hidden as of right now. Watch how do eject ⬇️ Link to your account setting with feature flag: [https://railway.app/account/feature-flags](https://railway.app/account/feature-flags?ref=funkyton.com) ![Feature flag setting in Railway](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-04-05-191504.png) Now, head back to your project, select the frontend container -> settings -> Upstream repo. Click "eject" - this will clone the repo to your github. ![How to eject git service from Railway](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-04-05-191616.png) ![Choose your Github username](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-04-05-191713.png) Sadly the cloned repository will be given a random name and is not private! But I have a feeling Railway might address this in the near future. Now, you should be aligned with templates that were deployed prior to this change - and you should be able to proceed with the tutorial, cloning your repo to your local machine, make code changes and push them to start the buildpipeline. Sadly, this new system does not facilitate monorepositories yet - so if you need to make changes to the backend, like adding a payment plugin or mail service plugin. You need to also eject the backend app, and it will clone it into another git repository, as it currently doesn't detect monorepositories. I surely hope railway will change this as well. ## Step 1: Clone repository to your local machine To locate your repo, select one of the app containers (frontend or backen), selected **settings** from the tabs in the view on the right. Scroll down to **Source repo** and click on it. ![How to find URL to your ejected source repository](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-29-104643.png) locate your source repository in railway dashboard If you get a 404 from GitHub, this is most likely because you are not logged in and you set the repository to be private during the creation. Login and try again. Locate your repository URL can copy it. ![How to clone your personal ejected repository to your local machine](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-29-105856.png) Open your preferred terminal or command prompt. Navigate to the directory where you want to clone the repository using the cd command. For example, if you want to clone the repository into a folder named "MyProject" on your desktop, you would use: ``` cd Documents/MyProject ``` Next, use the Git command to clone the repository: ``` git clone ``` ## Step 2: Open the project in Visual Studio Code Once the repository is cloned, you can open it in Visual Studio Code. Start VS Code and go to File > Open Folder.... Navigate to the folder where you cloned the repository, select it, and click Open. ## Step 3: Make changes to your project With the project open in VS Code, you can now make changes to the code. Navigate through the project files in the sidebar, open the files you need to edit, and make your changes. ## Step 4: Commit your changes After making changes, you need to commit them to your local Git repository. First, open the integrated terminal in VS Code by selecting Terminal > New Terminal from the top menu. In the terminal, add all changed files to the staging area using: ``` git add . ``` Then, commit the changes with a message describing what you have done: ``` git commit -m "Describe your changes here" ``` ## Step 5: Push changes to GitHub To push your changes to GitHub and trigger the build pipeline, use the following command in the terminal: ``` git push origin main ``` Assuming you are working on the main branch, this command will upload your changes to GitHub, updating your repository. Once your changes are pushed to GitHub, the automated build pipeline associated with your repository should start automatically. You can monitor the progress and outcome of the build by going to your Railway dahsboard. The storefront service should be marked with "building" When you get a green checkmark on your fronent service your change is live! 🥳 ### Ghost CMS 6: Deploy a Full Open Source Blog in 1 Click URL: https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/ Last updated: 2026-07-27T21:22:17.000Z This article started as a quick Ghost 5 deployment guide back in 2024. The idea was simple: make it easy to launch the full open source version of Ghost without needing to know Docker, Linux, database setup, or server administration. Two years later, I am still using this setup for this blog. That is worth pointing out: you are reading this article on a Ghost 6 blog right now. That is the real selling point for me. I have tried several CMS platforms over the years, but Ghost have remained my daily driver, because I have never needed more than simple. I want to write, publish, own my content, and keep control over my setup without turning my blog into another technical project. This template is my attempt to make that setup easy for other people too. It gives you a quick way to launch Ghost 6 with the full Admin dashboard, database, optional email support, optional image storage, and source code you can actually inspect and modify if you need to. ## Overview In this updated guide, I will cover: - [What this Ghost 6 quick launch template gives you](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#what-this-ghost-6-quick-launch-template-gives-you) - [Why I still use this setup after 2 years](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#why-i-still-use-this-setup-after-2-years) - [What is new in Ghost 6](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#what-is-new-in-ghost-6) - [How to deploy your own Ghost blog](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#how-to-deploy-your-own-ghost-blog) - [Optional Mailgun, Stripe and Cloudinary setup](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#optional-mailgun-stripe-and-cloudinary-setup) - [Who this template is best for](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#who-this-template-is-best-for) - [Final Thoughts](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#final-thoughts) ## What this Ghost 6 quick launch template gives you The goal of this template is simple: Self-host the full version of Ghost CMS, without the feature caps of the official managed hobby tier, at an even lower cost. More features. Full control. Lower hosting cost. After running this setup myself for more than 2 years, I can say that goal has been achieved. You get: - Full Ghost CMS 6 - Full Ghost Admin dashboard - MySQL database - One-click deployment - Optional Mailgun email support - Optional Cloudinary image storage - Source code unpacked in this [GitHub repository](https://github.com/rpuls/ghost-boilerplate?ref=funkyton.com) That last point is important. A lot of deployment templates are based on prebuilt Docker images. That can be fine, but it also means the setup can feel a little sealed off. You run it, but you do not really own the structure around it. This template is different because it runs directly on unpacked source code in GitHub. You can inspect how it works, update it, fork it, and adapt it if your needs change later. You do not have to do that to use the template, but having the option is exactly why I prefer this kind of open source setup. Not because I want to tweak everything all the time, but because I want the option when it matters. ## Why I still use this setup after 2 years I have now been running this template for more than 2 years, and this blog has been on Ghost for more than 5 years (previously on Heroku). That is long enough to know whether a setup is actually good, or whether it only looked good in the beginning. For me, this setup has passed that test. It gives me the things I care about most: full control over my blog, ownership of my data, low hosting cost, and a CMS that is simple enough that I actually enjoy using it. It has also stayed flexible when I needed it. The best example is image storage. At some point, I wanted uploaded images to live outside the application container, so I added Cloudinary support to the template. That is the kind of change that would be much harder if the setup was completely sealed away from me. But the biggest reason I still use this setup is simpler than that: I have never had a strong reason to switch. I have tried several CMS platforms over the years. Some were more powerful, some were more flexible, and some were more complicated. But for this blog, I have never needed more than simple. Ghost gives me a clean writing and publishing experience, and Railway gives me hosting that mostly stays out of my way. That is exactly what I want from a blog setup. If you are looking for a more advanced, more customizable CMS, and have the technical skills and patience to configure it, check out Payload. [Payload CMS 3 - Open source website builderDiscover the flexibility and control of Payload CMS, a fully open-source website builder. Explore its features, ease of use, and customization potential. Whether you’re building a blog or complex site, Payload offers endless possibilities. Deploy your own instance with our 1-click template.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Payload-CMS-3-deploy-free-on-railway-in-minutes-by-funkyton.png)](https://funkyton.com/payload-cms/) ## What is new in Ghost 6? Ghost 6 makes Ghost feel less like “just a blog” and more like a complete publishing platform. The biggest improvement is that Ghost now gives you a much better path from simple publishing to audience monetization. You can start with a normal blog, publish free articles, and build an email list. Then, if your audience grows, you already have the tools to turn that attention into income without moving to another platform. Think of it a little bit like a Patreon-style supporter model, but built directly into your own website. Ghost can now handle: - Free and paid members - Member-only posts - Paid newsletters - Monthly or yearly subscriptions - Payments via Stripe integration - Better audience tools - Native analytics That is a powerful idea. You can run a real publishing platform with ultra low hosting cost, more on that later, while keeping full control over your content and your audience relationship. But the best part is that you do not have to care about any of this on day one. If you just want a clean blog, Ghost still feels simple. You can ignore memberships, paid content, subscriptions, analytics, and newsletters until they become relevant. That is what I like about Ghost. The extra features are there when you need them, but they do not get in your way when you do not. ### ActivityPub support coming soon One of the most interesting Ghost 6 features is Social Web support through ActivityPub. In plain English, this means your Ghost publication can become followable from compatible social platforms like Mastodon, Threads, Bluesky, WordPress, Ghost itself, and other open social web apps. So instead of just publishing to your website and hoping people find it through Google, your content also becomes visible on a wider social network. People can follow your publication, see new posts in their feeds, and interact with them through likes, replies, and reposts. That is a huge idea for reach. Your blog stays the home base. Your content still lives on your own website. But your posts can travel further through the open social web, without depending only on newsletters, search engines, or closed social media algorithms. This template currently does not support ActivityPub, but I plan to add support for connecting it to Ghost's official ActivityPub service soon. For now, this template already gives you the full Ghost CMS, the clean publishing experience, the monetization tools, low hosting cost, control over your data, and simple deployment. ActivityPub will be a very nice next step. ## How to deploy your own Ghost blog This is the main selling point of the template: You can deploy a full Ghost 6 blog in around 3 minutes. You do not need to manually install Ghost. You do not need to configure MySQL yourself. You do not need to SSH into a server. You do not need to know Docker. You only need a Railway account to get started. The setup process is very simple, so you probably will not need a video guide. But if you get stuck during the Railway deployment flow, you can refer to this slightly outdated [video recording of the original Ghost 5 version](https://funkyton.com/ghost-on-railway-deploy-your-blog-with-1-click/#video-instructions). ## Prerequisites Required: - A Railway account Optional, but useful (can be added later): - A [Mailgun](https://www.mailgun.com/?ref=funkyton.com) account, if you want to send newsletters - A [Cloudinary](https://cloudinary.com/?ref=funkyton.com) account, if you want image optimization and external image storage - A [GitHub](https://github.com/?ref=funkyton.com) account, if you want to fork the source code and customize the template If you just want to test Ghost and see if this setup works for you, start with Railway only. You can add Mailgun, Cloudinary, and GitHub later. ### Deploy the template Click the deploy button below and follow the Railway setup flow. [ ![Deploy on Railway](https://railway.app/button.svg) v6.54 🤩 updated 27\. July 2026 ](https://railway.app/deploy/ZQcedl?referralCode=-Yg50p&ref=funkyton.com) ![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/ghost-cms-6-railway-template-overview-png.png?_a=BAMAPqiu0) ![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/how-to-deploy-ghost-cms-6-on-railway-png.png?_a=BAMAPqiu0) How to deploy Ghost CMS 6 on Railway Railway will create the project and start deploying Ghost together with the database. Give it a few minutes to finish the first build and startup. Once Railway shows that the deployment is live, open the generated public URL. ![How to access your Ghost CMS url from Railway dashboard](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/ghost-successfully-deployed-how-to-access-your-new-blog-with-the-public-url-jpg.jpg?_a=BAMAPqiu0) How to access your Ghost CMS url from Railway dashboard ### Create your Ghost admin user When the site is deployed, open your new Ghost URL in the browser. To create the first admin user, add `/ghost` to the end of the URL. For example: `https://your-ghost-site.up.railway.app/ghost` Ghost will then show the initial setup screen where you create the owner account for your publication. ![How to access the Ghost CMS admin user Signup/Login page](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/how-to-access-admin-dashboard-to-create-admin-user-for-newly-deployed-ghost-blog-site-jpg.jpg?_a=BAMAPqiu0) How to access the Ghost CMS admin user Signup/Login page ![How to signup your admin user in Ghost CMS](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/ghost-blog-admin-user-creation-form-jpg.jpg?_a=BAMAPqiu0) How to signup your admin user in Ghost CMS After that, you can log in to Ghost Admin and start editing your site. At this point, the core setup is done. You have a working Ghost blog, and you can start publishing right away. If you ever want to go beyond the basic setup, I have got you covered too. ## Optional Mailgun, Stripe and Cloudinary setup Ghost can be more than a place to publish articles. With a few extra services, it can also handle newsletters, paid memberships, and optimized image hosting. **Mailgun** is for newsletter delivery. If you want subscribers to receive your posts by email, this is the service Ghost uses for bulk email sending. **Stripe** is for paid memberships. If you want to offer monthly or yearly subscriptions, like a small Patreon-style supporter model, this is what handles the payments. **Cloudinary** is for professional image serving. It can make your images load faster, improve image optimization, help your SEO score, and take some load away from the Railway app. You do not need to set all of this up now. But if your blog grows, these are the integrations that turn Ghost from a simple blog into a real publishing platform. ⚠️ Note: I will not cover service signup, since these platforms change their onboarding screens often. This guide focuses on the settings you need after your accounts are ready. ### Mailgun for newsletters [Mailgun](https://www.mailgun.com/?ref=funkyton.com) is needed if you want to send newsletters from Ghost. You can write and publish normal blog posts without Mailgun, but if you want people to subscribe by email and receive your posts as newsletters, you should configure Mailgun in Ghost Admin. In Ghost, go to: `Settings → Email newsletter → Mailgun settings` Then add: - Mailgun region - Mailgun domain - Mailgun private API key ![How to configure Mailgun in Ghost CMS](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/how-to-configure-mailgun-in-ghost-admin-dashboard-api-key-and-domain-png.png?_a=BAMAPqiu0) Once this is configured, Ghost can use Mailgun for bulk newsletter delivery. This is optional, but highly recommended if you want to use Ghost as more than just a website. ### Stripe for paid memberships [Stripe](https://stripe.com/?ref=funkyton.com) is used if you want to accept paid subscriptions. This is the part that makes Ghost interesting as a small publishing business platform. You can start with a free blog, grow an audience, and later add paid membership tiers if it makes sense. Think of it like a Patreon-style supporter model, but built into your own website. In Ghost, go to the membership settings and connect Stripe. ![How to connect Stripe payments in Ghost CMS for membership subscriptions](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/how-to-connect-ghost-with-strip-to-enable-monetization-and-membership-subscription-png.png?_a=BAMAPqiu0) After connecting Stripe, you can set up free and paid tiers, member-only content, and monthly or yearly subscriptions. You do not need Stripe if you only want a normal free blog. But it is nice to know that the option is there if your audience grows. ### Cloudinary for image storage and optimization [Cloudinary](https://cloudinary.com/?ref=funkyton.com) is optional, but I personally recommend it for a serious blog. By default, Ghost can store uploaded images inside the normal application storage. That works, but I prefer keeping media storage separate from the app itself. With Cloudinary, uploaded images are stored outside Railway and served through Cloudinary instead. The benefits are: - Faster image delivery - Image optimization - Less storage usage on Railway - Less image traffic served directly from Railway - Easier migration if you ever move the blog later There is one trade-off: you are adding a third-party service for image hosting. For me, that trade-off is worth it. I want Ghost to run the CMS, MySQL to store the database, and Cloudinary to handle the media files. To set up Cloudinary, create a Cloudinary account and find your API details. You need: - Cloud name - API key - API secret Cloudinary shows these as a connection URL like this: `cloudinary://:@` ![How to locate your Cloudinary URL](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/how-to-get-cloudinary-api-url-png.png?_a=BAMAPqiu0) Then go to Railway, open the Ghost service, and add a new environment variable: `CLOUDINARY_URL` Use your Cloudinary connection URL as the value. ![How to add Cloudrinary URL to Ghost CMS in Railway](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/how-to-add-cloudinary-api-url-to-railway-ghost-environment-variables-png.png?_a=BAMAPqiu0) After adding the variable, click "Deploy changes". To test that it works, create a draft post in Ghost and upload an image. Then check that the image appears in Cloudinary, or inspect the image URL in the article preview. If the uploaded image is served from Cloudinary, the setup is working. One important note: keep your Cloudinary API secret private. Do not share screenshots with real secrets visible, and keep backups of your media in case you ever want to move away from Cloudinary later. ## Who this template is best for This template is best for individuals or small businesses who want the benefits of self-hosting without manually managing a server. It is a good fit if you want: - A personal blog - A project blog - A niche publication - A small business blog - A full open source CMS - Full control over your data - Lower hosting costs than managed CMS hosting - A setup you can customize later if needed I consider this setup production ready for the kind of blog or small publication I run myself. But it is still a slightly DIY solution. If you are a larger business with high traffic, serious revenue, strict uptime requirements, and a budget for managed hosting, I would probably not recommend this as your first option. In that case, I would use Ghost(Pro) Business or Enterprise instead, and let the official Ghost team handle hosting, scaling, support, and operational risk. For individuals, small businesses, side projects, and niche publishers, though, this template hits a very nice balance: Full Ghost CMS, low hosting cost, simple deployment, and enough control to modify the setup if you ever need to. ## Final thoughts After more than 5 years with Ghost, and more than 2 years running this Railway template, I still have not found a better blogging setup for my own needs. That says more than any feature list. I get the full open source Ghost CMS, full control over my content, the option to customize the source code, and a setup that has survived multiple major updates without becoming annoying to maintain. The hosting cost is also still almost hard to believe. This blog gets roughly 15k monthly visits, with around 2k genuine human visitors, and the Railway cost is still around $8 per month. ![Screenshot of the real server cost of this blog on Railway for June 2026](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/ghost-hosted-on-railway-resource-usage-hosting-cost-estimate-png.png?_a=BAMAPqiu0) Screenshot of the real server cost of this blog on Railway for June 2026 That number is not a promise. If your blog gets much higher traffic, congratulations, that is a good problem to have. You may also need to pay a little more for server resources. But for me, it proves the point. You can run a real publishing platform at a very low fixed cost, without giving up ownership of your content or your data. That is why I still recommend this setup. If you want to launch a modern open source blog without server management, this Ghost 6 quick launch template is a very solid place to start. ### Video instructions ⚠️ This video was recorded for the older Ghost 5 version of this template. Use it mainly if you get stuck with the Railway deployment flow. Ghost 6 has improved a lot since then, so the current Admin UI and features are much better than shown here. --- ### 3D print: QuadLock laptop webcam mount URL: https://funkyton.com/3d-print-quadlock-laptop-webcam-mount/ Last updated: 2025-01-29T14:56:41.000Z Download part: [https://www.thingiverse.com/thing:6542151](https://www.thingiverse.com/thing:6542151?ref=funkyton.com) Want to use your phone as a webcam, and already own a Quadlock case? This part might help you! Very easy to print, I used the default printer settings, as shown in the screenshot below. The support was removable almost without traces. **License notice:** I have been in contact with Quadlock regarding sharing a design that fits with their patented system - I thank them very much for being cool about it and allowing DIY communities to build on top of their platform! However, they made it very clear that selling these parts is against their policy, which is totally fair in my opinion. Therefore, please be aware of the license of the file, CC Non-Commercial: [https://creativecommons.org/licenses/by-nc/4.0/deed.en](https://creativecommons.org/licenses/by-nc/4.0/deed.en?ref=funkyton.com) Please be respectful about that. 🙏 ![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20240317_122346_sm.jpg) ![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20240317_122233_sm.jpg) ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20240317_122201_sm.jpg) ![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-20-222848.png) ### Tutorial: Medusajs on Railway - DIY ecommerce, has never been easier! URL: https://funkyton.com/medusajs-free-fully-open-source-ecommerce-solution/ Last updated: 2026-02-26T13:36:02.000Z ❗NEW VERSION ALERT: Medusa 2.0 has been released 👇 [Medusa 2.0 is finally hereIn this article you will find a 1-click deploy template, so with zero effort you can get your very own medusajs 2.0 preview instance, at its current state.![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/blog-cover.png)](https://funkyton.com/medusajs-2-0-is-finally-here/) ### But, if you prefer the original...: By following this tutorial, you will have a fully operational ecommerce solution; webshop, order and pruduct management - up and running, on a server that ***you*** manage. Medusajs is an amazing open source project, that by the time of writing this tutorial, is mature enough to replace most of the functionality offered by popular paid ecommerce solutions like shopify, woocommerce etc. ## Tutorial overview Difficulty: 4 / 10 Estimated time: 20-30 minutes Credit Card required: Yes, $5 ### What will you have after this tutorial? Here is a the webshop I created while writing this series of tutorials, check it out: 😶‍🌫️ the demo shop has been discontinued, (stay tuned for medusa 2.0 demo) ## Step 1: Sign up to GitHub and Railway If you don't already have a GitHub account, head over to [https://github.com/](https://github.com/signup?ref%5Fcta=Sign+up&ref%5Floc=header+logged+out&ref%5Fpage=%2F&source=header-home) and create a user. Once you have a GitHub account, you need to connect it to Railway. Head over to: [https://railway.app/](https://railway.app/?referralCode=-Yg50p&ref=funkyton.com) and sign in with GitHub ![Sign up with GitHub](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-182057.png) Once you are signed in, and have accepted Railways terms and conditions, you will find the page shown below. You need to upgrade to at least the "Hobby plan" in order to proceed with this tutorial. By the time of writing this tutorial, the hobby tier is $5/mo ![Choose hobby plan](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-174616.png) ## Step 2: Deploy medusajs template on Railway We will go straight to the hosting, and will be setting up local development environment later. I have chosen to use Railway for hosting, because they offer a hobby plan that is very cheap compared to the average paid ecommerce solution. And because Railway allowed me to create a one-click deploy template for this project - so you don't have to do much. [ ![Deploy on Railway](https://railway.app/button.svg) updated 21-06-2024 ](https://railway.app/deploy/QvfPwp?referralCode=-Yg50p&ref=funkyton.com) By clicking the button above, you will be taken to the deploy page: ![Template overview, click Deploy Now](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-161100.png) After clicking "Deploy Now" you need to confirm the pre-configuration of backend and frontend services. ![No need to configure anything, click Deploy](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/railway-configure-template.gif) Once you click the **Deploy** button, you're in business! You will get a project canvas and your app will start to build. It should that looks like this: ![Project canvas during build phase](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-194951.png) Within a minute your Postgres database and Redis instance should be ready. The two medusajs (Nextjs) apps however, will take a long time to build. The backend needs to finish a number of build steps which can take up to 7 minutes - maybe even longer. The frontend cannot build before the backend has finished, this is because the nextjs app will build pages based on data served from the backend. The entire process should hopefully not take more than 12 minutes. Once you have 4 green checkmarks your app is hosted! However, in order to really use it, there's one more building blocks we need to add. ## Step 3: Add a media storage service If you would login to your admin dashboard now, create a product and upload a few images it would work. However, those images would be saved on the disk (Railway's server) and would not be persisted after your next deploy. Therefore, we need to add an external storage provider, and in my opinion there does not exist a better option than Cloudinary. I have been using them for years and years, their customer service is outstanding, they are the most feature rich media service I have ever had my hands on, and... they let you start for free, with a pretty large complimentary usage before you need to pay. You will need to create a developer account, so head over to [https://cloudinary.com/](https://cloudinary.com/invites/lpov9zyyucivvxsnalc5/yhlpdo1vaw2mq1la0nks?t=default&ref=funkyton.com) and click the red button "Sign up for free". From here you can choose to signup with GitHub again, like we did for Railway, but this time it is not required, so feel free to sign up with email or google user. During the signup, Cloudinary will ask you to choose between two user types, choose the one that is related to "development and api calls" To get your API key, secret and cloud name, go to; Programmable Media -> Dashboard: ![Locate your Cloudinary API key](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-05-001607.png) Now you need to add these values to your Railway environment variables, in the backend container: ![How to add new variable in Railway](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-220036.png) This is also where you can change the secret value of JWT\_SECRET and COOKIE\_SECRET if you didn't already do that. Add the 3 required Couldinary environment variables like so, but use ***your own*** values: ![How to add all required Cloudinary environment variables](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-220203.png) Once added, you should see 3 changes in your backend container, these needs to be deployed in order to take effect, click deploy: ![Deploy changes](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-220830.png) ## Step 4: Setup completed! 🤩 To view your webshop, choose your frontend container -> deployments, and click the public url of the green box: ![How to find your publich URL for the storefront](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-222544.png) If everything went right, you'll see your webshop in a new tab with some dummy products: ![Webshop with demo products](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-04-225704.png) To manage your webshop, head over to the admin app by selecting your backend container, and clicking the Railway public url: ![How to find the public URL for your admin dashboard](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-05-002222.png) You will encounter this page, but don't worry: ![Modification to the url, to visit the dashboard route](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-05-002557.png) Your admin app is server on "your-url.com/app" so simple add /app at the end of the url and hit enter: ![Admin dashboard login screen](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Screenshot-2024-03-05-002829.png) To login use the defaul admin user: **Email:** admin@medusa-test.com **Password:** supersecret The tutorial ends here, I wont bother writing about adding products to your webshop or deleting the dummy items, the process is pretty self explanatory, and in case you get stuck, you can find the official user guide here: [https://docs.medusajs.com/user-guide](https://docs.medusajs.com/user-guide?ref=funkyton.com) ### To learn how you can customize your storefron visit the part II of this tutorial [Tutorial: Personalize your medusajs webshop (storefront)This tutorial is part II of the medusa on railway series, where we will be customizing your medusajs webshop! If you didn’t already complete step I please do so, in order to proceed with this part of the tutorial. OBS: Change at Railway! Please note that some information in the![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://static.ghost.org/v3.0.0/images/publication-cover.png)](https://funkyton.com/tutorial-customize-your-medusajs-webshop-storefron/) ### From Print to Play: A 3D-Printed and Handpainted Settlers of Catan Gameboard URL: https://funkyton.com/3d-printed-and-handpainted/ Last updated: 2025-01-29T14:47:26.000Z The traditional board game Settlers of Catan is very popular because it combines strategy and luck, while still being realatively easy to play. Despite the popularity, I think it could be even better with an upgrade to the gameboard and playing peieces. This post will feature a custom Settlers of Catan gameboard that was 3D printed and handpainted. Keep in mind that to play, you must still have all of the game cards from the original game. I spent a lot of time browsing Thingiverse for 3D files to use for my custom gameboard. There are hundreds, if not thousands, of Settlers of Catan-related designs available, and I picked out the ones that I liked the most and thought would fit together well. I used files from several different creators on Thingiverse, and I would like to give credit to the following: - [https://www.thingiverse.com/dakanzla/designs](https://www.thingiverse.com/dakanzla/designs?ref=funkyton.com) - [https://www.thingiverse.com/ajepuck/designs](https://www.thingiverse.com/ajepuck/designs?ref=funkyton.com) - [https://www.thingiverse.com/trevorclarke/designs](https://www.thingiverse.com/trevorclarke/designs?ref=funkyton.com) - [https://www.thingiverse.com/explosiveegg/designs](https://www.thingiverse.com/explosiveegg/designs?ref=funkyton.com) - And more. It took me about a year to print all the pieces for the gameboard on a resin 3D printer, as I had to pause the project several times while working on other projects as well. This project was very time-consuming, so it was nice to take breaks and work on something else for a change. The total time I used on printing adds up to about 3 days, even thought I made effort to fit as many pieces on the print plate for each print. Before painting, I had to insert magnets into all the tiles so that they would stay together on the gameboard. I used small ball magnets for this, which the tiles are specifically designed to fit. In total, I used 666 magnets (18 tiles x 36 magnets per tile). Inserting and gluing the magnets took about 6 hours. After inserting and gluing the magnets, I also sanded the bottom of the tiles on a belt sander to make them smoother. And only now, while writing the post I realize that I did not need to fill magnets in the part of the water tiles that makes up the outer perimeter of the board 🤦‍♂️.... ![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/tile-with-magnet-insert.jpg) ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/tiles-backside-magnets.jpg) Process of inserting magnets in game board tiles. To paint the tiles, I used an airbrush for the primer and to give the pieces a base color. I also used the airbrush to achieve gradient effects, such as on the water tiles. I finished off all the details with a very fine brush. Finally, I sprayed a layer of clear varnish on the water tiles and a layer of matte varnish on everything else. I mainly used airbrush paints from the Vallejo Game line, as these paints can achieve a cartoony look which I was going for. ![Tool used for painting](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/airbrush-paint-and-painting-tools.jpg) Tool used for painting ### Introducing VAMP - the 3D printed table lamp URL: https://funkyton.com/3d-printed-lamp-design/ Last updated: 2025-01-29T14:46:46.000Z Introducing the 3D printed lamp, a truly one-of-a-kind piece that combines classic design elements with modern, complex geometry. Almost all of the components of this lamp are 3D printed, including the body and shade, using a 3D printing process that consumes minimal plastic and produces no waste, as it is printed without any support material. The print process takes roughly 20 hours to complete. ## Shades ![](https://res.cloudinary.com/hczpmiapo/image/upload/v1669985008/ghost-blog-images/white-skirt-on-white-zigzag.jpg) ![](https://res.cloudinary.com/hczpmiapo/image/upload/v1669985007/ghost-blog-images/white-michelin-on-white-zigzag.jpg) ![](https://res.cloudinary.com/hczpmiapo/image/upload/v1669985007/ghost-blog-images/white-gear-on-white-zigzag.jpg) ![](https://res.cloudinary.com/hczpmiapo/image/upload/v1669985007/ghost-blog-images/white-classic-on-white-zigzag.jpg) ## Mounting system 'The design platform offers a shade mounting system that usiversally fit all the shades in this product line, but not only - it will also be compatible with many other shades from 3rd part manufactors. ![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20230212_134103_sm.jpg) ![](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20230212_134559_sm.jpg) ![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20230212_134415_sm.jpg) ![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20230212_134118_sm.jpg) Image gallery, 3D printed lamp system The mounting pins are printed with transparent resin, to make them as invisible as possible. No glue or screws are required in the assembly, the pins snap right in the holes, and are held in place with a lock ring. ## Disclaimer The electrical components of the lamp, including the bulb, socket, and wire, are sourced from a regulated consumer-grade production to ensure safety and reliability. The unique design of this lamp is a blend of classic table lamp elements and modern complex geometry, designed as an exterior for these few standard electrical components. ## A small introduction of "vase mode" printing The lamps are made using a unique 3D printing method called vase mode. With this technique, a single continuous spiral structure is used to create the object in one go, rather than building it layer by layer. This results in a smooth, seamless finish with no visible seam points. Additionally, the 3D printer moves at a constant speed throughout the entire printing process, eliminating common artifacts such as "ghosting" or "ringing" caused by acceleration or deceleration. This constant speed helps create a high-quality finish with minimal visible imperfections. Vase mode also has some design constraints. Because the extrusion is done in one go, it is not possible to use support structures, as this would require the extrusion to be stopped while the printhead is moved to where the support material was supposed to be printed. No support means no material is wasted, but it also means the design can't have too steep angles or overhangs, as these would not be possible to print without support. ## Inspiration and collaboration With permission of 3D modeller Jason A. Wright (WRIGHTMEDIA) I have remixed his vase model "cross cut spiral" and adapted it to be suiatble for this lamp design. The geometry of this vase is well known in the 3D community, many creators have made the same shape, and there are a number of tutorials on YouTube on how to model this exact shape - it's kind of a classic! It is also seen in glaced ceramic vases of slightly different body shape. ## Want to buy? ![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/wamp-skirt-black-zigzag-0__preview.jpg) #### 3D Printed Vase Lamp [SOLD OUT (contact)](https://funkyton.com/info) ### Print your own backgammon set! URL: https://funkyton.com/print-your-own-backgammon-set/ Last updated: 2025-01-29T14:46:04.000Z No backgammon? No problem! Feel free to download my 3d printable backgammon game, and get playing. The game includes the board with all the tiles and all the checkers, and is inspired by [Modellaner's smaller backgammon](https://www.thingiverse.com/thing:3349839?ref=funkyton.com). You will need 3 different colours of filament to make the game set, or you can use just one colour of filament and paint the printed pieces. The game is a rather large set compared to most of the available 3d printable backgammon games on the internet. The game measures: 242,5mm x 306mm x 13,5mm but only requires a print volume of 155mm x 250mm since it is made up of 2 half that are printed individually. I printed mine on a Creality ender 3 max. ![Complete backgammon game set including checkers](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/3d-printed-backgammon-full-game.png) Complete backgammon game set including checkers To assemble the game, you can either press the tile inlay into the board using force. I used a wood working clamp to squeeze them in place, this way they are not coming out again. Alternatively, you can scale them down 2% before printing - this should be easier to fit, but they will probably need a few drops of glue to be secured. If you wish you can insert neodymium magnets in the circular cut outs all around the edge. This way the 2 halves will lock together lightly. The magnets that fits the cut out are circular Ø: 4mm x 1,7mm, these can be bought for cheap on several website. You will need 20 magnets in total. ![Magnet dimensions](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mangnet-dimension.png) Magnet dimensions **Download backgammon from:** [**https://www.thingiverse.com/thing:5360397**](https://www.thingiverse.com/thing:5360397?ref=funkyton.com) ![Store chekers and dices in departments](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/3d-printed-backgammon-checker-storage.png) Store chekers and dices in departments ### DIY SONOS port / connect (IKEA symfonisk hack) URL: https://funkyton.com/diy-sonos-connect-ikea-symfonisk-hack/ Last updated: 2025-01-29T14:45:08.000Z In this article I will cover how I connected my crisp sounding studio monitors to the rest of my SONOS system, and for the small price of 99€. > Disclaimer: You can easily break your symfonisk speaker by following this guide! And more importantly, you will be exposed to the danger of extremely high voltage capacitor that can shock you also when the board is not plugged in! Be careful! First thing first, I am using the circuit board from the IKEA symfonisk speaker to to connect two active studio monitors via XLR. Unlike many other builds that uses the amplified output from the symfonisk module to drive passive speakers, I am using the non amplified signal, to connect my studio monitors since they have their own amplifiers - tuned to their drivers. This way I can add XLR speakers to my SONOS setup for the price of 99€, instead of 449€ that the SONOS port cost. Does this really work? The answer is "yes - but". I will cover my findings throughout this article. ### Shopping list: - IKEA symonisk speaker - XLR male connector x 2 ### Prerequisite: - 3d printer (optional, if reusing IKEA case) - Soldering iron + solder - Wires - Tools to cut plastic (I used a dremel) ### Download compact case: [https://www.thingiverse.com/thing:5346807](https://www.thingiverse.com/thing:5346807?ref=funkyton.com) ## Build guide ### 1: Disassemble symfonisk speaker I found this video the very helpful. After removing the front panel, unscrew all the electronics so that you are left with the circuit board only - but BE CAERFUL, there are 3 capacitors with massive charge in them, discharge them before touching the board further!! If you do not know what a capacitor is or how to discharge it, I wouldn't recommend you to attempt this modification, as it is simply too dangerous. Leave the heat sink in the IKEA speaker, we are not gonna need it since we are gonna be using the non amplified signal. You need to apply quite a bit of force to break free the Wi-Fi antennas, they are held in place with double sided tape - we are gonna be needing them for later, just leave them connected to the main board. Also disconnect the front button panel, 4 small screws (be sure not to lose these screws they are very important later), the flat cable can be unmounted by lifting up the tiny black plate of top of it, then the cable can be lifted up. ### 2: Solder wires to the non amplified sound signal You are gonna need some very steady hands and a very pointy soldering iron, because the space you need to solder within is tiny! *And by the way, this is the step where you might break your speaker if you do it wrong.* you are gonna need 3 wires, one for negative (I used yellow), one for low frequency out and one for the high frequency out (I used blue for these). Visit [https://www.instructables.com/Cheaper-Sonos-Architectural-With-IKEA/](https://www.instructables.com/Cheaper-Sonos-Architectural-With-IKEA/?ref=funkyton.com) and scroll down to **step 3: Solder to Board**. This guy have very good high resolution images of the solder points. Join the two lose ends of the (blue) high frequency and the (blue) low frequency wires so we get a joined full spectrum signal wire (blue). Next solder wires to your XLR connectors. Pin 1 and 3 is both ground, while pin 2 needs to be connected to the joined audio signal. I suggest wiring the XLR connectors in parallel leaving just one ground and one audio signal wire loose. This way, we easily can connect them with the lose wires from the mainboard ***after*** installing everything in the case. ![XLR parallel (mono) wiring](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/xlr-mono-parallel-wiring.png) XLR parallel (mono) wiring ### 3: The build First print the container that I have designed for the purpose of this build, you can find it at [thingiverse.com](https://www.thingiverse.com/thing:5346807?ref=funkyton.com). I printed it on my ender 3 max. ![CAD model of enclosure](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/symfonisk-case.jpg) CAD model of enclosure If you do not have a 3d printer, you could simply put everything back in the IKEA symfonisk case and drill holes for the audio connectors. First disconnect the flat cable from the button module, but keep it connect to the main board, it will be impossible to connect once the board is mounted in the box so keep it connected while you install the main board in the case. Use the same screws as were used to hold it in place in the IKEA enclosure. Now install the 220V power connecter in the oval whole right next to the main board. Use the same 2 screws as were used in the IKEA enclosure. Then connect it to the board. Last but not least, insert the 2 XLR male connectors in the case, you can secure them with hot glue or screws, but if you decided to use screws, I would suggest that you drill holes in the case first. ![Assembled box with XLR, Ethernet and power connectors](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/back-small-image.jpg) Assembled box with XLR, Ethernet and power connectors Now the back is complete, but we still need to fit the Wi-Fi antennas, and install the front panel. In order to fit the Wi-Fi antennas you need to cut away some of the plastic they are mounted to, otherwise they wont fit. They should look something like this: ![WiFi antennas](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20220409_133010.jpg) WiFi antennas Leave them like this while we focus on the front panel. Install the button module on the front panel. Use the same 4 screws as were used in the IKEA enclosure. Then connect the flat cable from the main board to the button module. Now connect the XLR ground wire (my yellow) to the ground wire you soldered to the main board, and then connect the audio signal wire (my blue) to the XLR audio wire. Everything is now connected, use the following image as reference on how to fit everything back in the casing, I found no other way of fitting the Wi-Fi antennas. ![How to fit everything inside the enclosure](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20220409_134004.jpg) How to fit everything inside the enclosure All you need to do now is to secure the front panel with 4 screws, I used excess screws from the symfonisk speaker. ![Finished box seen from the front](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/front-small-image.jpg) Finished box seen from the front ## The result Now that the build is complete you can plug in your set of studio monitor or PA system - basically anything that uses XLR as audio interface can now be connected to your SONOS system with this box. Plug in the power cable and wait for it to connect to your Wi-Fi. It will of course still be recognized by the SONOS app as a symfonisk speaker. I renamed my speaker to "Studio monitors" in the app. ### Does it work? It definitely gets your speakers online, and capable of using along with all the other SONOS products, for just 99€. However, this setup turns a set of stereo speakers into a mono setup, so that's something to keep in mind. You could of course spend 2 x 99€ and build the box twice, then wire just one speaker to each box and setup a stereo pair in the SONOS app. ### Sound quality? So I found that my studio monitors sounds much more deep when connected via this box than they usually do. So I think there might be some EQ going on before the signal is amplified, this is probably done to boost the bass of the symfonisk speaker. Here is a sound spectrogram of the studio monitors, and you can see they they are way too loud around the 125Hz frequencies. ![Audio analysis showing slightly over exposed 100hz range](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/monitors-before-reconnect.jpg) Audio analysis showing slightly over exposed 100hz range Compared to a spectrogram of the same song on my SONOS Beam (soundbar) which has a much nicer flat line. ![Audio analysis of original SONOS device, showing better frequency spectrum](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/beam-alone.jpg) Audio analysis of original SONOS device, showing better frequency spectrum I have tried to turn down the bass a little on in the SONOS app for the studio monitors, but it doesn't quite get the sound right. Unfortunately the app does not allow for advanced equalization, so I can't turn down the 125Hz area alone. ## Conclusion I will be using this setup when hosting house parties where sound quality is less important, and I will probably place the monitors in the garden and keep the real SONOS speakers in the house to have same music playing everywhere. But unless there is a way to fine tune the EQ to get the sound just right, I will call this solution unfit for audiophiles. I was expecting much better sound quality, as the studio monitors are able to reproduce sound much more accurate than any of my SONOS speakers, but due to the spike around 125Hz the sound is less accurate than the SONOS speakers ### 3D printed replacement CUE button for NUMARK dj controller URL: https://funkyton.com/3d-printed-replacement-cue-button-for-numark-dj-controller/ Last updated: 2025-01-29T14:44:20.000Z Download part from [thingiverse](https://www.thingiverse.com/thing:4944698?ref=funkyton.com) As DJs and music enthusiasts, we all know that not all equipment is built to last a life time. This is certainly true for some of the lower-end NUMARK DJ controllers, such as the MIXDECK. I have personally experienced this issue first hand, breaking a total of 3 CUE buttons on my own MIXDECK controller. Frustrated with the high cost of repair and the lack of quality in the replacement parts, I decided to take matters into my own hands. Introducing my custom-designed replacement button for the NUMARK MIXDECK controller. This 3D-printed button is not only cost-effective, coming in at around $0.50 for a standard resin print, but it could also offers a more durable solution than the original parts if printed with fiber reinforced resin, which should at max double the print price to $1\. The repair service I used in the past charged me $80 and, to my disappointment, they did not even replace the button unit, instead opting for a cheap fix with hot glue. In contrast, an original replacement button costs around $20 including shipping. With my 3D-printed button, you can save money and hopefully have a more reliable repair for your controller. I hope this solution can help others in the same situation, and that it will inspire others to think outside the box when it comes to repairing and upgrading their equipment. You can find the link to the 3D model below. Download [replacement cue button](https://www.thingiverse.com/thing:4944698?ref=funkyton.com) Controller: [https://www.numark.com/product/mixdeck](https://www.numark.com/product/mixdeck?ref=funkyton.com) Official replacement part: [https://instrumentalparts.com/button-painting-cue-large-pt1110636210/](https://instrumentalparts.com/button-painting-cue-large-pt1110636210/?ref=funkyton.com) ![installation instructions](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IMG_20211218_163113.jpg) installation instructions ### Fix 3D printer "unknown file format" error after updating chitubox URL: https://funkyton.com/fix-3d-printer-unknown-file-format-after-updating-chitubox-to-1-9/ Last updated: 2025-01-15T20:18:42.000Z If you are the owner of one of the following 3d printers: - CREALITY LD-002R - CREALITY LD-002H - CREALITY LD-006 And you are getting the error "unknown file format" when trying to prin a newly sliced file.ctb, it might be because your version of chitubox is higher than 1.7\. (both chitubox 1.8 and 1.9) now requires you to flash a little firmware update on to your printer. But don't worry, it is very easy. You can see the minimum required firmware version for your printer, of the latest version of chitubox on this linkt: [https://www.chitubox.com/en/download/chitubox-free](https://www.chitubox.com/en/download/chitubox-free?ref=funkyton.com) Head over to crealitys download page: [https://www.creality.com/download](https://www.creality.com/download?ref=funkyton.com) My printer is a LD-002R so I download the following file: ![creality-download-firmware-update-ld-002r](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/creality-download-firmware-update-ld-002r.png) Choose the file that is made for your printer. Now you need to unzip the file, format a usb flash drive, and the copy the update files onto the flash drives root: ![firmware-update-on-formatted-usb-chitubox-creality-resin-printer](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/firmware-update-on-formatted-usb-chitubox-creality-resin-printer.png) Lastly, you insert the usb drive in your prtiner, and then turn on main power. Your printer should show black screen and beep very loud for a few seconds. That means the updating is being installed. Please use this video as ref: If the firmware have successfully been flashed onto the hardware it should say: V4.4.3\_x1\_LCDKE, in system -> about. ### Wall switch cover with mount for IKEA TRÅDFRI smart home dimmer. URL: https://funkyton.com/wall-switch-cover-mount-for-ikea-dimmer/ Last updated: 2025-01-29T14:41:33.000Z > Download from: [Thingiverse](https://www.thingiverse.com/thing:4734663?ref=funkyton.com) > Dimmer found at: [IKEA wireless dimmer](https://www.ikea.com/nl/en/p/tradfri-wireless-dimmer-white-70408595/?ref=funkyton.com) There is nothing more annoying than when you smart home becomes dumber than it was before all the gadgets... This is something I experienced right from the start of my smart home journey. I bought Zigbee light bulbs from IKEA, in the believe that controlling them from my phone would be great. I was right and wrong. Turning on/off lights around the house at night from the bed, is super useful. But then there is the everyday use case, where you really just want to press a button to turn on light, it's a lot faster than unlocking your phone, and more importantly all your guests will only understand how to control your lightning with the physical switches. This is where the problem starts. I have found it to be impossible to teach my guests to use the zigbee dimmer switch and NOT TURN OFF the normal wall switch, because then the lightbulbs powerline is cut, and they become offline. When a bulb is offline it is impossible to control from what ever voice assistant, automation or app you might use, hence we want to avoid that... So the normal wall switch is a no go, and the wireless zigbee controller is not enough on its own, as most people simply will use normal wall switches by default. I wouldn't blame them, why would you ever press less standardized buttons in someone else home, compared to a traditional wall switch? In my opinion, building a smart home requires some thought to it, and its the responsibility of you who built it to make it useful for people who did not part take in building it. My solution to this problem, is a 3d printed cover that is mounted in the frame of the wall switch - and over the actual switch built into the wall. This way, no one can flip it! It's just a friction fit and it works very well, no screws or glue needed. The nice thing about this is that there will only be *the right button* to press, and it will be exactly where your guest would expect to find it. I recommend sticking a strong magnet on the backside of the cover to hold the dimmer tightly in place. ![CAD model of IKEA smart switch cover](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/IKEA-dimmer-mount-wall-switch-cover-3d-model.jpg) CAD model of IKEA smart switch cover The cover is designed to fit inside a common European wall switch. It measures 62.5 x 62.5 x 6.5 mm. If you have precise measurements of another common standardized wall switch, please contact me, and I will look into making a version of this cover that fits with your wall switch. ### Dishwasher chopstick holder URL: https://funkyton.com/dishwasher-chopstik-holder/ Last updated: 2025-01-29T14:42:15.000Z I frequently use meat needles, mainly as reusable sticks for olives, artichoke, sardines, etc. However they are completely dishwasher incapable, which is why I made this small container with a solid bottom, this way they easily go in the dishwasher. It doubles nicely as a chopstick holder and mounts easily to the cutlery organizer. ![Also functions as a meat needle holder](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/dishwasher-meat-needle-holder.jpg) Also functions as a meat needle holder Download from [Thingiverse](https://www.thingiverse.com/thing:4728710?ref=funkyton.com) ![Mounting clips](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/mounting.jpg) Mounting clips This print is very cheap, and only uses less than 10 ml of resin, furthermore it's very easy to print and doesn't need any support. Take a look at my print settings here: ![Printing instructions](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/dishwasher-chopstick-holder-print-settings.png) Printing instructions ### 5$ hack for your Air Purifier that will improve your health URL: https://funkyton.com/diy-upgrade-activated-carbon-air-purifier/ Last updated: 2025-01-20T22:44:53.000Z This simple hack will turn your regular; air purifier, dehumidifier or air conditioner into an advanced air cleaner that will remove anything from odors to toxins and even radioactive particles from your indoor environment. ## Is this really necessary? The one thing more important than food and drinking water is the air we breathe. Every day we breathe around 10,000 liters of air, so why wouldn't we want it to be clean? Now I do not want to provoke fear, but there are a lot of reasons why the air in your house could be bad for you and your family. Furniture, wall paint, building materials all slowly release toxins into the air, especially cheap new furniture - not to mention the most obvious, smoking (do it outside - really!). Furthermore, the worst air in your house is generated when cooking. In fact, the air quality while cooking can rapidly increase the particle count to many times higher than the streets of the most polluted cities in the world. You might think that street pollution is much worse, and yes, especially diesel particles are probably the worst, but the particles from your cooking are still extremely bad, and you should not want to breathe it. Then there is radon, a radioactive gas emitted from the ground and certain building materials such as concrete. Radon will bind to the dust in your house and make the dust particles radioactive, which will accumulate in your lungs and body over time and at a certain point, the chance of lung cancer will no longer be insignificant. The list of reasons why your air could be bad goes on. Background pollution from the outside air, if you live in populated cities, can be so bad that ventilation won't bring down the household pollution to a healthy level. And all this is why... you may want to consider purifying your indoor air. > WHO: In 2016, household air pollution was responsible for 3.8 million deaths, and 7.7% of the global mortality ## What can you do to protect yourself? The first and most practical solution is to ventilate a lot. Open your windows frequently, especially while and after cooking. But sometimes this is not always a good option, letting out heat is expensive, and how can you even be guaranteed that the new air you let in is perfect? The chances are that if you live next to a moderately trafficked road, the outside air isn't really all that good. The next important point is to clean frequently, by dusting off and vacuuming you will get far. But even with these two rational actions done, there is still a lot of room for improvement.... A half year ago, I decided to buy two air purifiers, and boy was I surprised by the amount of indoor pollution that they measured and reported back to me! I have left them both on auto mode, and several times a day I hear them spin up the fan to clean the air, trying to bring the PM2.5 particle count down to below 20 PM2.5 per cubic meter of air. With these two air purifiers, I am able to get my indoor environment much better than the outside air. This is good, but all this made me wonder... can I do better? The HEPA filters in the air purifiers aren't able to filter out everything that is bad from the air. And now that I had already dipped my toes into the huge subject of air quality with a health perspective, I decided to do additional research. Without sharing too many numbers and case studies, I want to boil it down to one interesting thing: activated carbon. It turns out that gas masks, advanced bunker air filters, factory air filters, water filters, and many more technological filtration devices all use activated carbon to filter out difficult toxins. The next thought that naturally came to me was: "how can I combine the filtration properties of activated carbon with my air purifiers?" I started searching online, and it turned out that you can already buy activated carbon filters for most air purifiers. Good, right? Yeah... but there is one problem: the price tag. The cheapest filters I am able to find are priced around $50, which isn't that bad. But it's still already half the price of my air purifier, including its normal filter - just for a short-lasting filter. I decided to make my own extension and was able to find all the parts I needed to upgrade my normal HEPA filter to an activated carbon filter for around $5\. Here is what you need. ## Part list: ### Air Purifer I'm using a xiaomi Air Purifier 2H, this is a relatively cheap air purifier priced around 100€/120$ including an excellent large hepa filter. The really awsome thing about this air purifier and the whole xiaomi product line is that almost all of them uses the exact same hepa filter. You can find the [2S on amazon](https://www.amazon.com/gp/product/B07CZB1KZL/ref=as%5Fli%5Ftl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B07CZB1KZL&linkCode=as2&tag=codegravy-20&linkId=def54db0d52c2717a716595b33c2a4e6&ref=funkyton.com) which is quite a bit more expensive than the 2H version, but uses the exact same filter. ### Extractor hood filter cloth This can be picked up very cheap in most hardware and home appliances stores, or can be found on [Amazon](https://www.amazon.com/gp/product/B08HQ2SQWM/ref=as%5Fli%5Ftl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B08HQ2SQWM&linkCode=as2&tag=codegravy-20&linkId=0ab4c2466149aecec154b8334e02028f&ref=funkyton.com). But you can also cut open a vacuum cleaner bag and use that instead. ### Air vent Just by searching in one hardware store I was able to find one very cheap (3$) that fits exactly in my HEPA filter. ![air-vent](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/air-vent-filter-for-purifier-125mm.png) But in case you own a 3d printer or know someone who does, I conveniently designed this part for you free of charge ### Activated carbon pebbles This can be found cheap when buying larger amount, but you only need a little. [Activated carbon on Amazon](https://www.amazon.com/gp/product/B01F1QZTQC/ref=as%5Fli%5Ftl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B01F1QZTQC&linkCode=as2&tag=codegravy-20&linkId=52e26736553bee41ebd4e469dcdf3361&ref=funkyton.com) cost 24$ but you only need a fraction of this amount, and might be able to find it in smaller amounts elsewhere. ## Build guide ![Off the shelf air vent, gets 2nd purpose as carbon filter](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/cut-filter-cloth-to-match-air-vent.jpg) Off the shelf air vent, gets 2nd purpose as carbon filter Cut filter cloth to match the inner diameter of the air vent. ![Place the filter cloth inside the air vent](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/place-the-filter-inside-the-airvent.jpg) Place the filter cloth inside the air vent Place the filter cloth inside the air vent, and make sure it aligns nicely on the edge all around. ![Fill the air vent with activated carbon pebbles](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/fill-air-vent-with-activated-carbon-pebbles.jpg) Fill the air vent with activated carbon pebbles Fill the air vent with activated carbon pebbles all the way to the top, make sure there is no room left. ![Cut more filter cloth to cover the air vent](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/cut-a-larger-piece-of-filter-cloth.jpg) Cut more filter cloth to cover the air vent Cut a larger piece of filter cloth. The diameter should be large enough to easily cover the air vent with some excess. ![Seal the air vent, secure the active carbon pebbles](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/tighten-the-filter-cloth-around-the-airvent.jpg) Seal the air vent, secure the active carbon pebbles Seal the open part of the air vent with the large piece of filter cloth, secure it with a rubber band, sticky tape or string. This should secure the activated carbon pebbles between two layers of filter cloth. ![Insert carbon modified air vent back into hepa filter](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/install-the-activated-carbon-filter-in-the-hepa-filter.gif) Insert carbon modified air vent back into hepa filter Install the activated carbon filter in the HEPA filter. Take a moment and enjoy the satisfaction of two completely different products fitting perfectly together, almost like its meant to be? ![Insert hepa filter back in air purifier and and turn it on](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/reinsert-the-hepa-filter-in-the-air-purifer.gif) Insert hepa filter back in air purifier and and turn it on Reinsert the HEPA filter in the air purifier and make sure it runs normally. That's it. ## Sources: Most polluted cities: [https://en.wikipedia.org/wiki/List\_of\_most-polluted\_cities\_by\_particulate\_matter\_concentration](https://en.wikipedia.org/wiki/List%5Fof%5Fmost-polluted%5Fcities%5Fby%5Fparticulate%5Fmatter%5Fconcentration?ref=funkyton.com) Air pollution: [https://www.who.int/data/gho/data/themes/topics/topic-details/GHO/ambient-air-pollution](https://www.who.int/data/gho/data/themes/topics/topic-details/GHO/ambient-air-pollution?ref=funkyton.com) Air pollution when cooking or smoking: [https://airqualitynews.com/2019/07/05/exposure-to-pm2-5-greater-cooking-an-omelette-than-london-roadside-study-claims/](https://airqualitynews.com/2019/07/05/exposure-to-pm2-5-greater-cooking-an-omelette-than-london-roadside-study-claims/?ref=funkyton.com) ### Turn Raspberry Pi + external HDD into NAS URL: https://funkyton.com/local-mass-storage-container-with-raspberry-pi-and-external-hdd/ Last updated: 2025-01-20T22:43:09.000Z This guide will help you setting up a network drive so that you can access your external HDD from all of your devices over WiFi. NAS is short for "Network Attached Storage" - let's attach your HDD to your home network! OBS: If you came here after installing PMS, you can skip the apt-get update && apt-get upgrade. But if you are just here for the NAS setup we will need to be up to date. ``` sudo apt-get update sudo apt-get upgrade -y ``` Next install samba ``` sudo apt-get install samba samba-common-bin ``` Now let's configure samba to expose your external HDD as a network drive ``` sudo nano /etc/samba/smb.conf ``` In the nano editor add the following: ``` [nasberrypi] path = /disks/plexmedia writeable=Yes create mask=0777 directory mask=0777 public=no ``` Leave everything else as is, save and exit. I like to call my network drive "*nasberrypi*" but feel free to use another name. If the directory/hdd that you want to expose is not in "/disks/plexmedia" please change the path to your desired location. You need to set a password for samba by using the command: ``` sudo smbpasswd -a pi ``` It will ask you for a password and a confirmational retype. ## Add network drive to your windows machine Let's add your network drive to This PC as if it was just another physical HDD in your computer. ![Map network drive on windwos](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/map-samba-network-drive-to-windows.png) Map network drive on windwos **This PC** \-> **Map network drive** \-> **Map network drive** ![Map network drive setup wizard on windows](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/add-samba-drive-to-windows.png) Map network drive setup wizard on windows In the setup wizard choose a Drive letter, for example "X:" In the Folder input you need to write "\\\\\\" following my example it should be: \\\\raspberrypi\\nasberrypi Tick both check boxes and hit Finish. Windows will ask for a password, enter the one you chose just a minute earlier and click OK. ![Network drive added to windows PC](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Samba-drive-added-to-windows-this-pc.png) Network drive added to windows PC [Install Plex Media Server on Raspberry Pi OSWhat you need \* Raspberry Pi 3 or Raspberry Pi 4 (better performance) \* micro sdcard min 8GB - preferably class 10 (like this one) \* sdcard reader \* putty (download from: https://www.putty.org/) \* ethernet access to your router \* ip scanner, download: https://www.advanced-ip-scanner.com/ (opt…![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/turn-raspberry-pi-into-plex-media-server.png)](https://funkyton.com/install-plex-media-server-on-raspberry-pi/) ### Add external hard disk to Plex Media Library (on PMS) URL: https://funkyton.com/add-external-hard-disk-to-plex-media-library/ Last updated: 2025-01-20T22:42:04.000Z Assuming that you have already installed PMS on you raspberry pi or whatever little Linux computer you are using, next obvious thing would be to add storage. Most SBCs (single board computer) run the OS from an sd card and those are not ideal for storing large media libraries. This article will cover how to add an external HDD to you raspberry pi and allow PMS to browse the hard disk for media files. ### Create media directory By default Linux does not allow applications shuch as Plex Medis Servicer to manage an external HDD. First we need to create a directory where we want the meadi to be found, we want this directory to have read/write access so that PMS can read the files in it, and write to it if necersarry, for example when deleting media. Run all the following commands: ``` sudo mkdir /disks sudo mkdir /disks/plexmedia sudo chown -R pi:pi /disks sudo chmod -R 755 /disks ``` You should now have a directory /disks/plexmedia with the correct permission for the pi user. To check that this is true win command: ``` ls -la /disks ``` Which should output something like this: ``` total drwxr-xr-x 3 pi pi 4096 Aug 21 22:22 . drwxr-xr-x 22 root root 4096 Aug 21 22:22 .. ``` ### Connect your hard disk First connect your external hard disk to the raspberry pi, if you are using a pi 4 make sure to plug the USB cable into the blue ports, those are USB 3.0 and will transfer and read much faster. After connecting wait a few seconds to make sure Linux registers the new dives. Then run command: ``` sudo fdisk -l ``` This will list the disks and partitions, also the ones on your sd card. Try to find your harddrive in the list. Mine looks like this: ``` Device Start End Sectors Size Type /dev/sda1 34 262177 262144 128M Microsoft reserved /dev/sda2 264192 11721043967 11720779776 5.5T Microsoft basic data ``` So the "/dev/sda2" is what I am interested in configuring since this is the partition on my external hard disk with 5.5TB storage, this is where I will be placing all my media files and where I want Plex to read them from. Next we need to mount this disk: ``` sudo mount /dev/sda2 /media ``` If this gives you an error, your disk might be in have been formatted to another file system. Try to run the command again with the "type ntfs" option: ``` sudo mount /dev/sda2 /media -t ntfs ``` If this also doesn't work, you should format the disk entirely to the ntfs file system, then it should work. I will not go into details how to do this, there should be plenty of guides if you google. After mounting we need to gather some information about the disk. Use the blkid command: ``` sudo blkid ``` In my case my disk was "/dev/sda2", so my command looks like this: ``` sudo blkid /dev/sda2 ``` The out put you get should be similar to this below. We are interested in the disks UUID in my case "B83A843F7A83PHPH" ``` /dev/sda2: LABEL="Seagate Backup Plus Drive" UUID="B83A843F7A83PHPH" TYPE="ntfs" PTTYPE="atari" PARTLABEL="Basic data partition" PARTUUID="0add94c9-XXXX-4ed8-XXXX-d0a6398945a1" ``` ### Point mounted disk to the media directory ``` sudo nano /etc/fstab ``` add line: ``` UUID=B83A843F7A83PHPH /disks/plexmedia ntfs defaults,auto,rw,nofail 0 1 ``` (Replace "B83A843F7A83PHPH" with the UUID of your disk when adding the line) Save and exit. Now unmount media and mount plexmedia ``` sudo umount /media/ sudo mount /disks/plexmedia/ ``` Now the disk of your UUID should be pointed to the /disks&plexmedia directory, which we have set read/write pemissions for the pi user for. This should mean that you can now browse your external HDD from with PMS. 🎉 ![Add external drive from Plex interface](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/browse-media-files-from-external-hdd-plex.png) Add external drive from Plex interface The next article will cover how you can send files to this "plexmedia" directory from all of you other devices over WiFi, and basically use it as a NAS (Network Attached Storage) [Turn Raspberry Pi + external HDD into NASThis guide will help you setting up a network drive so that you can access your external HDD from all of your devices over WiFi. NAS is short for “Network Attached Storage” - let’s attach your HDD to your home network! OBS: If you came here after installing PMS, you![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/raspberry-pi-nas-setup.png)](https://funkyton.com/local-mass-storage-container-with-raspberry-pi-and-external-hdd/) ### Install Plex Media Server on Raspberry Pi OS URL: https://funkyton.com/install-plex-media-server-on-raspberry-pi/ Last updated: 2025-01-20T22:41:34.000Z ### What you need - Raspberry Pi 3 or Raspberry Pi 4 (better performance) - micro sdcard min 8GB - preferably class 10 ([like this one](https://amzn.to/39WOqzG?ref=funkyton.com)) - sdcard reader - putty (download from: [https://www.putty.org/](https://www.putty.org/?ref=funkyton.com)) - ethernet access to your router - ip scanner, download: [https://www.advanced-ip-scanner.com/](https://www.advanced-ip-scanner.com/?ref=funkyton.com) (optional) - basic linux terminal knowledge ### Prepare sdcard with headless Raspberry Pi OS (raspbian) We want to install the headless version of raspbian aka. Raspberry Pi OS since we are building a server and not a player. We do not need the visual GUI desktop interface, in fact we don't want to waste CPU and RAM on running that, instead we want all the resources to be dedicated to PMS (Plex Media Sever) for best performance. Download [Raspberry Pi Imager](https://downloads.raspberrypi.org/imager/imager.exe?ref=funkyton.com) from the official raspberry pi website [https://www.raspberrypi.org/downloads/](https://www.raspberrypi.org/downloads/?ref=funkyton.com) **Choose OS** \-> **Rasbperry Pi OS (other)** \-> **Raspberry Pi OS Lite** Next choose your sdcard and finally click the **Write** button. ![Raspbery Pi Imager choose operating system for PMS](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/install-pi-os-lite.png) Raspbery Pi Imager choose operating system for PMS When your imager is finished writing to your sdcard take it our of you card reader and insert it again. Your operating system should recognize it as a drive, open it up in explorer (windows) or finder (mac). We need to add a empty file by the name "ssh" with no extension. This file will enable SSH so that we can access the linux terminal over LAN with putty. ![Remove file exstionsion](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/create-extensionless-ssh-file.png) Remove file exstionsion Now the sdcard is ready. Insert it in your raspberry pi, connect it to your router with ethernet cable and finally connect the power supple to run it on. *Note: it is possible to make it work over WiFi as well, but it is far less stabel and quite a bit more complicated to configure, if you for some reason MUST use WiFi, I suggest using google to find a recent guide on how to connect with WiFi and the moving on with the rest of this guide when you are connected.* ### Log in to your new Linux installation through SSH (putty) After booting up eventually your device will get an IP from your router so that we can access it remotly. Next thing up is to find out what ip has been assigned to your pi. I'm using advanced ip scanner to find the ip of my pi, but you can also login to your router and see a list of connected devices. Copy the ip and open a ssh connecting with putty. ![SSH into linux on Raspberry Pi with putty](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/find-raspberry-pi-ip-adress.png) SSH into linux on Raspberry Pi with putty Your default username: **pi** And password: **rasbperry** ![Login with username and password](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/login-to-raspberry-pi-os.png) Login with username and password ### Install PMS (Plex Media Server) on linux/Rasbperry Pi OS Let's make sure we are up to date, run the follow two commands. (This might take a while) ``` sudo apt-get update sudo apt-get upgrade -y ``` We need to add the https transporter to be able to install packages over secure connection run: ``` sudo apt install apt-transport-https -y ``` Now let's add the plex signing key and repo ``` curl https://downloads.plex.tv/plex-keys/PlexSign.key | sudo apt-key add - echo 'deb https://downloads.plex.tv/repo/deb public main' | sudo tee /etc/apt/sources.list.d/plexmediaserver.list ``` Install PMS (Plex Media Server) ``` sudo apt update sudo apt install plexmediaserver ``` ### Visit your personal Plex Media Player for the first time Now you are good to go, you should be able to access your Plex instance from any device connected to the same local network as the PMS is running on. Go to: ``` http://192.168.1.10:32400/web/index.html ``` You should meet a setup wizard which should be fairly easy to follow, but then you are all set. ![Plex dashboard from browser, accessed via local IP](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/Access-your-personal-plex-media-player-from-laptop-phone-or-tablet.png) Plex dashboard from browser, accessed via local IP Please read next article which will help you configure your Plex media library and setup network storage that you can access from all of your devices. [Add external hard disk to Plex Media Library (on PMS)Assuming that you have already installed PMS on you raspberry pi or whatever little Linux computer you are using, next obvious thing would be to add storage. Most SBCs (single board computer) run the OS from an sd card and those are not ideal for storing large media libraries. This![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/connect-external-hdd-to-plex-and-use-for-media-storage-library.png)](https://funkyton.com/add-external-hard-disk-to-plex-media-library/) ### Rolling 3D dice with simulated physics in unity. URL: https://funkyton.com/rolling-virtual-dice-with-physics/ Last updated: 2025-01-20T22:40:47.000Z You have probably already tried at least a dozen virtual dice games right? Whether it have been simulated casino, virtual dice roller apps, or maybe a mini game in a PC or console game. I bet you that all of them have one thing in common. They are FAKE! > *disclaimer: This is not a step by step tutorial, but covers some higher level considerations when making a dice roll game.* Yes, usually virtual dice games doesn't actually roll the dice. Most likely a simple random number generator will choose a number between 1 and 6, followed by an animation that will end up showing the corresponding dice face to the number the random generator picked. To be fair here, there is absolutely nothing wrong about simulating dice rolls that way. It doesn't change the players chances in the game and the rolls are still unpredictable. Or to be precise - unpredictable unless you know "*the seed*" to the random generator and are able to read the memory of the program. But let's just consider it unpredictable for the sake of this article. For our simple Yahtzee score sheet app we wanted a dice roll that felt a little more realistic than the ones we have tried. First we designed a 3d dice in Blender, then a cup and imported both into Unity. Unity ships with a physics engine so you don't have to code your own from scratch, which comes in quite handy when you want to simulate physics for 3d objects. We gave the dice a rigidbody, a box collider and a material. We then tuned the friction and bounciness of the material until we were satisfied with the behavior of the dice when dropped onto a surface within unity. At this point we already have something that looks quite nice, except.... the dices makes the exact same roll each time and that totally strips away the realistic feeling. ![Dice falling out of cup, using simulated physics](https://res-2.cloudinary.com/heureka-studio/image/upload/q_auto/v1/blog-images/yahtzee-rolls.png) Dice falling out of cup, using simulated physics ## So how do we combine randomness, with physics that always behave the same? To be straightforward honest... We used a random number generator :) But we are not choosing a random number between 1 and 6\. How would that even go into the physics sequence? There would be a total 6 different rolls? That would be very boring as even the slowest mind would realize the repeating animation within a few games. Instead we decided to take advantage of the three dimensions x, y, z. Each dice is placed inside the cup where they are not visible to the player. The random generator then picks a number between 1 and 360 for each of the three axis and rotate each dice accordingly. ```csharp public void putInCup(){ if (firstRoll) { cupPosition = gameObject.transform.position; firstRoll = false; } if (!locked) { gameObject.transform.position = cupPosition; gameObject.transform.Rotate ( new Vector3 ( Random.Range(0f, 360f), Random.Range(0f, 360f), Random.Range(0f, 360f) ) ); gameObject.SetActive (false); } } ``` That gives us an almost infinite amount of positions the dices can be places, as the number between 1 and 360 is a decimal number (float) and there are 5 dice. What is the chance that 5 (dice) ⨯ 3 (dimensions) ⨯ 360 (degrees) ⨯ 10,000,000 (7 decimals) will ever roll the same? That leaves us with a total of 54,000,000,000 different rolls that will each land on one of 7776 different possible combinations. ## Pros and cons of using simulated physics to roll a dice So you might be thinking, was it worth all the trouble? What are the drawbacks? It might seem like a lot of work building a dice rolling game in 3d with a physics engine with gravity, bounciness, friction etc. But when you think about it, the only code to write is; resetting the dice position, calculation 3 random number (one for each axis), activating the dices so that the physics apply, in this case gravity will pull them down towards the plane surface and your roll is complete. You even get the animation for free instead of writing scripts that will animate images of dice faces. So what are the possible drawbacks? Well... first of all, since this is very random and there are so many different rolls that we don't have control over, we do sometimes end up with dices in an awkward position. I would estimate that about 1 in 30 rolls one of the dices will land on top of, or lean against another dice making that roll invalid. But this is no different from playing with dices in real life! Another possible drawback are the chances. We know how many total rolls there are based on the previous calculation, we also know how many possible combinations 5 dice can roll. We don't however, know if all of our 54 billion rolls are equally spread on the 7776 resulting combinations. If a very dedicated person would do statistics and collect enough data to prove that some combinations would occur more than other, they could use this information to gain advantage over other players. But again, that (kind of) also exist with real dices, some of them will have unequal distributed weight and will land on one side more often which could also be proven with statistics and then used to play strategical, which is what I'm gonna base my conclusion on. Rolling virtual dices with simulated physics is FUN! it not only looks awesome but also have a way more realistic feeling to it, and most important it is very easy to build. If you ever consider to make your own dice rolling game we will be happy to discuss any questions you might have. ### Chasing Lights in unity with UI image fade helper URL: https://funkyton.com/chasing-lights-unity/ Last updated: 2025-01-20T22:40:13.000Z In this article we gonna create two very simple image fading functions that are completely indispensable if you want to create a vibrant lively UI for your game or app. In this example we will be animating "chasing lights" such as seen on slots machines, and the exchange sign in the image above. ### The helper functions These two functions can be used in a lot of situations, I have personally used them to simulate light bulbs, flashing tutorial guide arrows, ghosting game characters etc. Let's get started. ```csharp using System.Collections; using UnityEngine; using UnityEngine.UI; public class ImageHelper { public static IEnumerator FadeOutAlpha(Image image, float fadeTime) { float elapsedTime = 0.0f; Color c = image.color; while (elapsedTime < fadeTime) { yield return new YieldInstruction(); elapsedTime += Time.deltaTime; c.a = 1.0f - Mathf.Clamp01(elapsedTime / fadeTime); image.color = c; } } public static IEnumerator FadeInAlpha(Image image, float fadeTime) { float elapsedTime = 0.0f; Color c = image.color; while (elapsedTime < fadeTime) { yield return new YieldInstruction(); elapsedTime += Time.deltaTime; c.a = Mathf.Clamp01(elapsedTime / fadeTime); image.color = c; } } } ``` I have created a class called ImageHelper, this class usually contains a few more helper functions but I have trimmed it down to just the two that we will be focusing today. FadeInAlpha and FadeOutAlphe, both takes an Image component and a fade time value as arguments. ### Apply the helper functions to images In the following script I have used the two helper functions to make two functions, one for turning light off and one for turning light on. Notice that the turn off function have twice as much fade time, this is to create the effect of and old incandescent bulb that keeps emitting light shortly after it is turned off due to the heat in the filament. Now that I can turn on and off light bulbs it is time to create a routine that will turn on a light and turn off the one before it, then do the same all over again for the next bulb in the line and repeat. Let's create a Image\[\] that we can reference our target images (our lights) from the unity editor, and then also build a looping routine function "ChasingLights". ```csharp using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Exchange : MonoBehaviour { [SerializeField] private Image[] lights; private bool running = false; void Start() { StartCoroutine(ChasingLights()); } private IEnumerator ChasingLights() { int index = 0; int iterations = (int) System.Math.Floor((float) lights.Length / 2); foreach (Image light in lights) { TurnLightOff(light); } while (index <= iterations && running) { yield return new WaitForSeconds(0.035f); TurnLightOff(lights[index]); TurnLightOff(lights[index + iterations]); TurnLightOn(lights[index]); TurnLightOn(lights[index + iterations]); index = (index >= iterations) ? 0 : index + 1; } } private void TurnLightOn(Image img) { StartCoroutine(ImageHelper.FadeInAlpha(img, 0.1f)); } private void TurnLightOff(Image img) { StartCoroutine(ImageHelper.FadeOutAlpha(img, 0.2f)); } } ``` ![Detailed GameObject setup example in unity inspector](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chasing-lighst-unity-setup.png) Detailed GameObject setup example in unity inspector I have my exchange sign with a lot of turned off light bulbs, I have then added 26 children (lights), these are then added to the Lights list in the exchange script and the routine in the script will take care of the rest and fading in/out these lights according to the routine. ### Looking for more easy game effects? [Unity “Blink Pulse” Sprite AnimationCreate a simple “blink pulse” sprite animation in Unity with this lightweight script. Adjust minimum alpha, maximum alpha, and cycles per second to control the fade effect. Attach it to your sprite GameObject for an easy, customizable pulsating animation that enhances your game’s visuals!![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/sprite-fade-cover.gif)](https://funkyton.com/unity-blink-fade-sprite/) [Unity audio fade in/out function (C#)Create seamless audio transitions in Unity with this simple C# helper function. Learn how to easily fade audio in and out using coroutines for smooth sound effects in your game. Perfect for enhancing player immersion without complex coding.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/cover.png)](https://funkyton.com/unity-audio-fade-in-out-function-c/) ### Unity "Blink Pulse" Sprite Animation URL: https://funkyton.com/unity-blink-fade-sprite/ Last updated: 2025-01-20T22:39:06.000Z Learn how to create a simple and effective "blink pulse" sprite animation in Unity. This step-by-step guide will teach you how to fade a sprite's alpha in a pulsating pattern using a lightweight script. ### Why Use a Blink Pulse Animation? Adding a blink pulse effect can bring life to your game objects, making them visually dynamic and engaging. Whether you're designing a glowing button or a magical aura, this effect is versatile and enhances the overall user experience. ### The Script: Lightweight and Customizable In my script folder, I keep a "micro" folder filled with tiny, reusable scripts like this one. These scripts are designed for quick implementation—just attach them to your GameObjects and tweak the parameters as needed. Here's the code: ```csharp public class blinkPulse : MonoBehaviour { private SpriteRenderer sr; public float minimum = 0.3f; public float maximum = 1f; public float cyclesPerSecond = 2.0f; private float a; private bool increasing = true; Color color; void Start() { sr = gameObject.GetComponent(); color = sr.color; a = maximum; } void Update() { float t = Time.deltaTime; if (a >= maximum) increasing = false; if (a <= minimum) increasing = true; a = increasing ? a += t * cyclesPerSecond * 2 : a -= t * cyclesPerSecond; color.a = a; sr.color = color; } } ``` ### Key Parameters The script has three adjustable parameters: - `**minimum**`: The lowest alpha value (e.g., `0.3` for 30% opacity). - `**maximum**`: The highest alpha value (e.g., `1` for full opacity). - **`cyclesPerSecond`**: Controls the speed of the pulse; higher values mean faster animation. ### How to Use It 1. Attach the script to your sprite GameObject in Unity. 2. Adjust the `minimum`, `maximum`, and `cyclesPerSecond` values in the Inspector panel to suit your needs. 3. Hit play, and watch your sprite come alive with a smooth pulsating glow! ![Example of gameObject with blink pulse script attached](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/sprite-fade.gif) Example of gameObject with blink pulse script attached Attach the script to your sprite GameObject, and you are good to go! ### More simple to implement, yet immersive effect: [Chasing Lights in unity with UI image fade helperCreate vibrant UI animations in Unity with simple image fade functions! Learn how to build “chasing lights” effects, like slot machines or exchange signs, using reusable helper functions for fading UI images.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chasing-lights.gif)](https://funkyton.com/chasing-lights-unity/) [Unity audio fade in/out function (C#)Create seamless audio transitions in Unity with this simple C# helper function. Learn how to easily fade audio in and out using coroutines for smooth sound effects in your game. Perfect for enhancing player immersion without complex coding.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/cover.png)](https://funkyton.com/unity-audio-fade-in-out-function-c/) ### Delete browser history for individual sites in Chrome on iOS URL: https://funkyton.com/delete-browser-history-for-individual-sites-in-chrome-on-ios/ Last updated: 2025-01-20T22:38:44.000Z This step by step guide will show you have to delete specific browser history records. Unlike on android, Chrome for iOS still have no way of deleting all data related to a domain. But this is the closet you can get. ### Step 1 ![Locate the settings button in chrome, followed by the "history" menu item](https://res-4.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/iOS-chrome-delete-step1.png) Locate the settings button in chrome, followed by the "history" menu item In Chrome locate the three dots menu button. Next press on "History" ### Step 2 ![Press the "Edit" button in the bottom of the history menu](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/iOS-chrome-delete-step2.png) Press the "Edit" button in the bottom of the history menu In the history menu you will see an overview of all browsing history, press the "Edit" button. ### Step 3 ![Now, select the sites your want to clear data for, then hit the Delete button](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/iOS-chrome-delete-step3.png) Now, select the sites your want to clear data for, then hit the Delete button Now select all the recorded visits that you would like to remove from your history, then click "Delete". This will remove only the selected record, if you have visited the same site previously those record will still remain in your browsing history, unlike on android where you are able to delete everything liked to a domain. ### Looking for a guide for android? [How to clear browser data for a single site in chrome on android. Step by stepStep by step guid with screenshots on how to delete everything from history, cookies, local storage, cache for individual websites.![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/cover-image.png)](https://funkyton.com/clear-cache-and-browser-history-for-a-single-site-in-chrome-on-android/) ### JavaScript optional chaining - failsafe way of accessing object properties that might or might not exist. URL: https://funkyton.com/javascript-optional-chaining-fail-safe-way-of-accessing-properties-that-might-or-might-not-exist/ Last updated: 2025-01-14T21:50:36.000Z One thing that makes JavaScript a very powerful programming language is its ability to create complex data structures in no time. I'm of course talking about JavaScript objects that you can define on the go without the need of creating generic classes and writing getters and setters for every property. But easy to construct often comes with easy to break. In this post we will look at a few different ways of working with JavaScript objects in a failsafe manner, so that we can access nested object properties when they exist but also prevent our program from crashing when they don't exist. ### Let's first understand the problem (what not to do) ```javascript const product = { id: 'id127781', name: 'AAA batteries' }; console.log(product.id); // id127781 console.log(product.price); // undefined console.log(product.specification.weight); // Uncaught TypeError: Cannot read property 'weight' of undefined ``` In this example we are trying to access price which doesn't exist and will be returned as undefined. We are also trying to access "weight" which should exist under "specification", but for some reason doesn't for this product. This is not good because when trying to access a property on "undefined", the engine will trow an error like the comment shows. Let's try to prevent this! ### Method 1 - *"or empty object"* ```javascript const product = { id: 'id127781', name: 'AAA batteries', appearance: { color: 'black' } }; const weight = ((product || {}).specification || {}).weight; console.log(weight); // undefined const color = ((product || {}).appearance || {}).color; console.log(color); // black // bonus: you can define fallback values very easy const weight = ((product || {}).specification || { weight: 'N/A' }).weight; console.log(weight); // N/A ``` This is one of my favorite ways of trying to access a deep leaf property in a data model where some properties are optional. *Weight equals product or empty object dot specification or empty object dot weight.* In this case product is not undefined so we do not replace it with empty object. Now we are trying to access specification on product, but specification does not exist so we get an empty object. At last we are trying to access weight of empty object which doesn't exist so we get undefined. This way we prevent asking for a property on undefined and therefore also prevent the program from crashing. ### Method 2 - ramda.js path ```javascript import * as R from 'ramda'; const weightUnit = R.path(['specification', 'weight', 'unit'], product); console.log(weightUnit) // undefined ``` If you are able to work with npm libraries ramda.js comes in very handy for a lot of really solid JavaScript missing fundamentals. The path function is specifically made for this common problem of safely trying to access deep nested objects values. You can learn more about ramda and all their amazing functions at the documentation page: [https://ramdajs.com/docs/#path](https://ramdajs.com/docs/?ref=funkyton.com#path) ### Method 3 - experimental optional chaining If you have ever worked with programming languages like Swift or Kotlin, you have probably been using the ?. operator frequently? (which probably origins from some older programming languages, but since I am not a dinosaur, I know it from the newer stuff). According to mozilla and other sources this feature might exist in the world of JavaScript soon. ```javascript const weightUnit = product?.specification?.weight?.unit; console.log(weightUnit); // undefined ``` IMPORTANT: this is not support yet in a variety of browsers! Currently it should be supported by Chrome v80+ and with the use of babel. You can read more about this feature from reliable sources right here: - [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional\_chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional%5Fchaining?ref=funkyton.com) - [https://v8.dev/features/optional-chaining](https://v8.dev/features/optional-chaining?ref=funkyton.com) ### Unity audio fade in/out function (C#) URL: https://funkyton.com/unity-audio-fade-in-out-function-c/ Last updated: 2025-01-14T22:08:24.000Z ## **Introduction** Smooth audio transitions are essential for creating immersive experiences in games. Whether you're fading out background music during a scene change or fading in sound effects for dramatic moments, this guide will show you how to implement a simple and effective audio fade-in/out function in Unity using C#. ### **What is Audio Fading?** Audio fading refers to gradually increasing or decreasing the volume of an audio source over time. This technique is commonly used in games to ensure seamless transitions between soundtracks or to enhance the emotional impact of a scene.In this article, you’ll learn how to: - Create a reusable helper function for audio fading. - Use Unity’s coroutines to control the fade duration. - Apply the function in your game project. ### **Step 1: Create the Helper Script** Start by creating a new C# script called `AudioHelper.cs`. This script will contain two static methods: one for fading out and another for fading in. ```csharp using System.Collections; using UnityEngine; public static class AudioHelper { public static IEnumerator FadeOut(AudioSource audioSource, float fadeTime) { float startVolume = audioSource.volume; while (audioSource.volume > 0) { audioSource.volume -= startVolume * Time.deltaTime / fadeTime; yield return null; } audioSource.Stop(); } public static IEnumerator FadeIn(AudioSource audioSource, float fadeTime) { audioSource.Play(); audioSource.volume = 0f; while (audioSource.volume < 1) { audioSource.volume += Time.deltaTime / fadeTime; yield return null; } } } ``` ### How to use in your project Since this function is an IEnumerator it must be started as a coroutine. In this example I get the audiosource from the gameObject the script is attached to. I set fadeTime to 2 seconds. ```csharp private AudioSource soundtrackAudioSource; private float fadeTime = 2f; void Start() { soundtrackAudioSource = GetComponent(); StartCoroutine(AudioHelper.FadeIn(soundtrackAudioSource, fadeTime)); } void StopMusic() { StartCoroutine(AudioHelper.FadeOut(soundtrackAudioSource, fadeTime)); } ``` ### **Testing and Debugging Tips** - **Testing**: Ensure your AudioSource component is properly set up with an audio clip assigned. Test different fade durations to find what works best for your game. - **Debugging**: If the audio cuts off abruptly, check that the coroutine is running correctly and that no other scripts are modifying the AudioSource volume. ### **Advanced Customization** You can enhance this functionality by: - Allowing dynamic fade times based on gameplay events. - Fading multiple AudioSources simultaneously for complex soundscapes. - Integrating with Unity’s Timeline feature for more precise control over audio transitions. ### **Why Use This Approach?** This method is lightweight, reusable, and easy to integrate into any Unity project. By using coroutines, you avoid blocking the main thread while achieving smooth volume transitions. ### **Conclusion** With this simple helper function, you can easily implement smooth audio fading in your Unity projects. Whether you're a beginner or an experienced developer, this solution is both really simple, but get the job done in most cases. Try it out in your game and see how it enhances your player’s experience! ### Now that you have mastered sound, why not add some visual effects? [Unity “Blink Pulse” Sprite AnimationCreate a simple “blink pulse” sprite animation in Unity with this lightweight script. Adjust minimum alpha, maximum alpha, and cycles per second to control the fade effect. Attach it to your sprite GameObject for an easy, customizable pulsating animation that enhances your game’s visuals!![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/sprite-fade-cover.gif)](https://funkyton.com/unity-blink-fade-sprite/) [Chasing Lights in unity with UI image fade helperCreate vibrant UI animations in Unity with simple image fade functions! Learn how to build “chasing lights” effects, like slot machines or exchange signs, using reusable helper functions for fading UI images.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)FunkytonRasmus Puls![](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chasing-lights.gif)](https://funkyton.com/chasing-lights-unity/) Have questions or suggestions? Let me know in the comments below! ### Generate react.js elements from markdown like text URL: https://funkyton.com/lightweight-react-js-text-parser-function/ Last updated: 2025-01-14T21:37:19.000Z ### This recursive function will format your text nicely into react elements. No library required. When building modern web apps you will often come across the issue of rendering text stored in either json files, or perhaps from a database entity, or maybe text fetched from an endpoint. This is a quite fundamental necessity for many apps but it does come with some problems to solve. For example, how do you store a linebreak? Let's solve the linebreak first and then add a bit more functionality afterwards. If you don't care about how it works you can [scroll down](#final-result) to the final code snippet that contains the working function. ### Step 1\. Line breaks Let's start by importing react, since we are gonna be using the createElement function. Next up, we will create the function that will generate linebreaks based on its input, I call it "textToHtml". Commonly used for linebreaks is \\n (backslash n), we will follow this convention. First create an if statement like below, that checks if the input contains any linebreaks. If there is at least one linebreak detected we are gonna return a div that wraps all of our

elements (paragraphs), next up we are splitting the input on each linebreak so we end up with an array containing separate strings, one for each linebrak and no string will contain any additional linbreaks. Finally we return each string as a paragraph with the key prop i, so that react can distinguish the elements from each other. In case there is no linebreak detected in the if statement we will in the else clause just return a paragraph the entire input. We end up with this function: ```javascript const React = require('react'); const html = React.createElement; const textToHtml = (input) => { if (input && (input.indexOf('\n') !== -1 || input.indexOf('\\n') !== -1)) { return ( html('div', {}, input.split(/\n|\\n/).map((paragraph, i) => { return ( html('p', { key: i }, paragraph); }))); } else { return ( html('p', {}, input)); } }; module.exports = { textToHtml }; ``` Now this is great, we are able to store text outside our app that are capable of being formatted with linbreaks. Just remember to add \\n whenever you want your text to do a linebreak. Pretty quickly you will probably need more complex text in your app than just plain text with linebreaks. That could for example be bold text. Let's add a function that can produce bold, talic and underlined text. ### Step 2\. Match: bold, talic & underline We will in this step be creating three different functions. First function is the most simple one, it will take three arguments, input (the text), element (the html element we want to use), regex (a pattern that will match our formatting language). If the regex matches anything we will add the element to the match object before returning it. Your function should look like this: ```javascript const matchElement = (input, element, regex) => { const match = regex.exec(input); if (match) match['element'] = element; return match; }; ``` Now that we have a function that is capable of detecting a pattern in a string and will return a match object containing the string (matched text) and an element type to use for generating the corresponding html tag. Now we need a function defining all the patterns and all the corresponding elements. Why not call it "matchAll"? ```javascript const matchAll = (input) => { const bold = matchElement(input, 'b', /\*\*([ a-z0-9!?.*_]+)\*\*/gmi); const talic = matchElement(input, 'i', /__([ a-z0-9!?.*_]+)__/gmi); const underLine = matchElement(input, 'u', /_\*([ a-z0-9!?.*_]+)\*_/gmi); return [bold, talic, underLine].filter(e => { return e != null; }); }; ``` Our matchAll function should look something like this, but you can customize yours to whatever you prefer. This one matches: \*\*bold\*\* \_\_talic\_\_ \_\*underlined\*\_ Finally we return all match objects (technically they are arrays). Remember from previous function (matchElement), that match object contains the text between the format tags/symbols, and we have added an element type so we know which html element should be used for this string. We filter the list and remove all nulls. To understand what's going on with that regex.exec(input) function, I have created a small example based on one of my unit tests. ```javascript const text = 'Today is a **happy** day. __Hurra__'; console.log(matchAll(text)); [ '**happy**', 'happy', index: 11, input: 'Today is a **happy** day. __Hurra__', groups: undefined, element: 'b' ] [ '__Hurra__', 'Hurra', index: 26, input: 'Today is a **happy** day. __Hurra__', groups: undefined, element: 'i' ] ``` ### Step 3 - Recursive formatting function Now we have a way of getting the text in small bites. The bites contains the string that we care about at index 1, plus information about which html element that string should be wrapped in on index 'element'. Now we can start building a recursive function that will format one bite, and then call it self with the remaining bites plus everything that is already formatted. At last if there is nothing more to format, we should return everything. ```javascript const formatParagraph = (unformatted, formatted) => { const matches = matchAll(unformatted); if (matches.length > 0) { const targetMatch = matches.sort((a, b) => { return a['index'] - b['index']; })[0]; const remaining = unformatted.substring(targetMatch['index'] + targetMatch[0].length, unformatted.length); const newElement = h(targetMatch['element'], { key: targetMatch.index }, formatParagraph(targetMatch[1])); const processed = [formatted, [unformatted.substring(0, targetMatch.index), newElement]]; return formatParagraph(remaining, processed); } else { return formatted ? [formatted, unformatted] : unformatted; } }; ``` This function is obviously a little more complex compared to the previous ones, but I will try to break it down to something understandable. I have called it formatParagraph because it formats the text in each paragraph, in other words the text between each linebreak. We are going to call this function from our textToHtml function, because that was where we divided the text into paragraphs based on the backslash n linebreak. The first argument of this function is called unformatted, because this is the text we give the function that have not yet been formatted. First off we run the matchAll function and we give it the unformatted text, we then receive all the matches as shown in the example above with the "happy day" sentence. If there is more than zero matches, we will convert each match into a nicely formatted react element which finally will be rendered as a html element. We will start from one end, formatting the first match in the text. To do so we are gonna sort the matches by index low to high, we can then grab the first match and store it in a variable called targetMatch. Next thing we want to worry about is the remaining text, to do that we create a variable called "remaining" where we store the unformatted text minus this target text. Now let's create the react element of type tagetMatch\['element'\], this was where we stored the html tag, we also must always remember the unique key prop, and finally we will give it the text. But wait... What if the text inside the matched pattern also contains another element to be formatted? It could for instance be \_\_\*\*bold and talic\*\*\_\_. That's right, this is a valid case and we will support that by doing our first recursive call with that text which will then be processed the same way. Finally we are gonna build up what have been processed. First we add "formatted" the second argument of the function, the first time the function is called this argument in undefined, but as the function keeps calling it self it is important to remember to add the already formatted text to the start of "processed". Next thing we want to add is all the text that was before the matched pattern, example: hello \_\_world\_\_, our match only focuses on the word "world", but we don't want to loose "hello" on the way, we add this text and finally we add the react element that we created. At last we call the function again, with the remaining text + the processed text, and this will happen all over again and again and again until all the text have been formatted and finally we will return it. ### The final result (all the code) This is the final result you can copy-paste this code directly into your react project and start using it, feel free to customzie it as much as you want, you could for example add more html tags, or you could use
tags for linebreaks instead of creating a new paragraph each time you want to do a linebreak, that is totally up to you. I have added a small replace in textToHtml, don't mind this too much, sometimes when I have received text from a database where the single quote have been replaced with its unicode ' I have replaced this with an escaped single quote. ```javascript const React = require('react'); const html = React.createElement; const textToHtml = (input) => { const text = input.replace(''', '\''); if (text && (text.indexOf('\n') !== -1 || text.indexOf('\\n') !== -1)) { return ( html('div', {}, text.split(/\n|\\n/).map((paragraph, i) => { return ( html('p', { key: i }, formatParagraphtml(paragraph))); }))); } else { return ( html('p', {}, formatParagraphtml(text))); } }; const formatParagraph = (unformatted, formatted) => { const matches = matchAll(unformatted); if (matches.length > 0) { const targetMatch = matches.sort((a, b) => { return a['index'] - b['index']; })[0]; const remaining = unformatted.substring(targetMatch['index'] + targetMatch[0].length, unformatted.length); const newElement = html(targetMatch['element'], { key: targetMatch.index }, formatParagraphtml(targetMatch[1])); const processed = [formatted, [unformatted.substring(0, targetMatch.index), newElement]]; return formatParagraphtml(remaining, processed); } else { return formatted ? [formatted, unformatted] : unformatted; } }; const matchAll = (input) => { const bold = matchElement(input, 'b', /\*\*([ a-z0-9!?.*_]+)\*\*/gmi); const talic = matchElement(input, 'i', /__([ a-z0-9!?.*_]+)__/gmi); const underLine = matchElement(input, 'u', /_\*([ a-z0-9!?.*_]+)\*_/gmi); return [bold, talic, underLine].filter(e => { return e != null; }); }; const matchElement = (input, element, regex) => { const match = regex.exec(input); if (match) match['element'] = element; return match; }; module.exports = { textToHtml }; ``` ### Why use this function instead of a fancy feature packed markdown parser? There's pros and cons to every choice. And this might not be a viable solution for your projects need. However, I have been working on large node.js projects for a long time, and something that keeps becoming more and more frequent in these mega repositories is what I call the "npm dependency hell". At my current day job we have a monthly chore called "npm outdated" this task is about updating dependencies so that our codebase doesn't end up in an un-updatable state because it is simple too far behind. Usually this task takes 1-3 full working days because several of the updates have deprecated APIs that you were using 12312 instances in your code, and will have to find a way to deal with this. Another thing is security, you don't know what security holes might be pulled when you do an update, luckily there are tool such as npm audit to help you monitor your packages for security breaches, but again this might result in more work as you suddenly have to stop using some module that you were depending on because it have not yet been patched. Small functions like the one doesn't have those disadvantages, and further more, I think there is a certain quality aspect of software that is built of code that the developers have actually had their eyes on. Anyway, this is a subject for a blog post itself, and I will leave it up to you whether you want to use a simple function like this in your code or depending on a fancy module that offer a lot more features (also features you don't need) and that develops over time and might cause you headache and cost time in the future. ### If you are gonna use it and care about code coverage... In case you want to use this function in your project and do care about code coverage but don't bother writing tests when you can simply skip it. First one's always free for a reason. ```javascript /* eslint-env mocha */ const format = require('./formatHtml'); describe('format html from text (markdown like)', function () { const input = 'Hello world!\nToday is a **happy** day. __Hurra__\n_*Cheers!*_'; const output = format.textToHtml(input); it('should return one div', function () { (typeof output).should.equal('object'); output.should.include({ type: 'div' }); }); it('should create 3 paragraphs', function () { output.props.children.should.have.length(3); output.props.children[0].should.include({ type: 'p' }); output.props.children[0].props.children.should.equal('Hello world!'); output.props.children[1].should.include({ type: 'p' }); }); it('second paragraph should contain child elements', function () { output.props.children[1].props.children[0][0][1][1].should.include({ type: 'b' }); output.props.children[1].props.children[0][0][1][1].props.children.should.equal('happy'); output.props.children[1].props.children[0][1][1].should.include({ type: 'i' }); output.props.children[1].props.children[0][1][1].props.children.should.equal('Hurra'); }); it('third paragraph should contain an underlined element', function () { output.props.children[2].props.children[0][1][1].should.include({ type: 'u' }); output.props.children[2].props.children[0][1][1].props.children.should.equal('Cheers!'); }); }); describe('format html from text (markdown like)', function () { const input = '___***WORD!***___'; const output = format.textToHtml(input); it('should return nested elements', function () { output.props.children[0][1][1].should.contain({ type: 'i' }); // outer output.props.children[0][1][1].props.children[0][1][1].should.contain({ type: 'u' }); // 2nd output.props.children[0][1][1].props.children[0][1][1].props.children[0][1][1].should.contain({ type: 'b' }); // inner output.props.children[0][1][1].props.children[0][1][1].props.children[0][1][1].props.children.should.equal('WORD!'); }); }); ``` *This test requires mocha and chai dev-dependencies.* ### JavaScript shuffle array (randomize) URL: https://funkyton.com/javascript-shuffle-array-randomize/ Last updated: 2025-01-20T22:35:35.000Z ### Fast, easy and simple function that randomizes any JavaScript array. Copy the code below to one of your existing project. Keep in mind that this function will return the same array as it is given after the array have been shuffled, it will not create a copy. ```javascript function shuffleArray (arr) { for (let i = arr.length - 1; i > 0; i--) { const r = Math.floor(Math.random() * (i + 1)); [arr[i], arr[r]] = [arr[r], arr[i]]; } return arr; }; ``` ### See it in action See the Pen [JavaScript Shuffle Array](https://codepen.io/rasmus-puls/details/VwLmJew?ref=funkyton.com) by codegravy ([@rasmus-puls](https://codepen.io/rasmus-puls?ref=funkyton.com)) on [CodePen](https://codepen.io/?ref=funkyton.com). Click the shuffle button to randomize the array. Click the restore button to return to initial array order. Codepen.io link: [https://codepen.io/rasmus-puls/pen/VwLmJew](https://codepen.io/rasmus-puls/pen/VwLmJew?ref=funkyton.com) ### How to clear browser data for a single site in chrome on android. Step by step URL: https://funkyton.com/clear-cache-and-browser-history-for-a-single-site-in-chrome-on-android/ Last updated: 2025-01-20T22:35:16.000Z This is a step by step guide with screenshots on how to remove browser data for a single website. This tutorial will help you clean cookies, local storage and cache from any website from your browsing history. You can even remove a single page from your browser history without removing data for any other websites. ### Step 1 Go to chrome and press the three dots to open the main menu. ![Locate the menu button in chrome on android](https://res-5.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chrome-delete-cache-step-1.jpg) Locate the menu button in chrome on android ### Step 2 Locate "Settings" in the bottom of the main menu. ![Locate the Settings menu item](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chrome-delete-cache-step-2-cut.jpg) Locate the Settings menu item ### Step 3 Scroll all the way to the bottom and click on "Site settings". ![Locate "Site settings" under Advanced](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chrome-delete-cache-step-3.jpg) Locate "Site settings" under Advanced ### Step 4 Scroll all the way down once again, find "Storage" click it. ![Locate the "Storage" section the the Site Settings](https://res-1.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chrome-delete-cache-step-4.jpg) Locate the "Storage" section the the Site Settings ### Step 5 This is the view where you can select just a single page. Select whatever page you want to delete data from. *Bonus: notice in the bottom "Clear site storage" this button is used to delete data from ALL sites.* ![Find the site you want to delete data from](https://res-3.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chrome-delete-cache-step-5.jpg) Find the site you want to delete data from ### Step 6 Click the "Clear & reset" button to delete from browser history, this will also delete cache as well as local storage and cookies ![Press the "Clear & reset" button](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/chrome-delete-cache-step-6-cut.jpg) Press the "Clear & reset" button ### Looking for iOS guide? [Delete browser history for individual sites in Chrome on iOSThis step by step guide will show you have to delete specific browser history records. Unlike on android, Chrome for iOS still have no way of deleting all data related to a domain. But this is the closet you can get. Step 1 In Chrome locate the three dots menu![](https://funkyton.com/favicon.png)FunkytonRasmus Puls![](https://res-2.cloudinary.com/hczpmiapo/image/upload/q_auto/v1/ghost-blog-images/iOS-chrome-delete-cover.png)](https://funkyton.com/delete-browser-history-for-individual-sites-in-chrome-on-ios/)