# Chirag Aggarwal's Blog Posts This file contains all blog posts from Chirag Aggarwal's personal website. Generated automatically from MDX files during build. --- ## My Journey in Authorization with OPAL **Slug:** my-journey-in-authorization-with-opal Before we even begin, many of you like my a-month-old self will wonder what even is Authorization, and especially... OPAL?? So let's break them down one by one. Starting with Authorization. ## Authentication vs Authorization ![Authentication vs Authorization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctcbpynn0b1cwme7ze42.png) Well, I started the article off with just one term, `Authorization`. Then why am I covering `Authentication` as well? Well because they are quite similar and can be easily confused with. **Authentication** is the process of _identifying_ an user. It tells us "who" the user is. For a website, whenever a user visits it, all it sees is an IP address asking for a document, that the server then renders and sends. To differentiate multiple requests, it needs to authenticate the request, more specifically the user calling the request. **Authorization** on the other hand is the step that comes _after_ authentication. It's the process of identifying what the user is allowed (authorized) to do. In simple words, it's the set of permissions that a particular user must follow depending upon who he/she is. ## OK but why? ![Why Authorization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ygo1fds4h7233gygvghd.png) I think most of you would have already understood why we need Authentication. Without websites would have no idea who you are! You will be another random Guest visiting the website. No wonder all the websites have `Signup/Login` as a basic functionality nowadays. But why do we need Authorization??? Authorization determines if the user has the permission to do a particular task. For eg. on a blog website, all the users might have permission to read your blog but only **you** can edit it. Incorrect or not setting up an authorization policy at all can lead to a lot of.... bad things 💀. ## Policies and OPA ![Policy in OPA](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1c55h9vv2pjzxfj7lif1.png) Just above I mentioned a term, `policy`. What is it? Policy is just a set of rules to establish the authorization system. There are many ways to write these policies. The one we will be covering here is called OPA. OPA, short for Open Policy Agent, is a high-level declarative language used for writing policies. It allows you to define the policies in a single language that can used across many parts of your system, rather than relying on vendor-specific technologies. ## OPAL and the problems with OPA While OPA is great for decoupling the policy from code in a highly performant and elegant way, it suffers from keeping the policy up to date as the requirements change on a day-to-day basis. This is where OPAL, Open Policy Agent Layer, comes and provides **real-time** policy updates. It continuously runs in backgrounds and updates the policy agents whenever needed. For eg. A user created a private blog page. So a policy was created so that it can only be accessed by the creator. But layer he/she wanted to allow access to a specific user with a given email ID. So OPAL can update the policy in real-time to allow that to happen. ## Conclusion ![Conclusion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1wnonldramx6rhlef2w6.png) After all this, you might have two types of thoughts. Some people thought "Wow this was amazing and I was totally missing out on this". But a mass majority, like me when first learning all this, thought "Why do we need all this just for authorization. Just used an If/Else call". And the funny thing is, for especially your use case, you might be right! You really don't need this much complexity for your average 10-user SaaS application. But this becomes essential for companies handling millions of users like Netflix, T-Mobile and Goldman-Sachs, who all use OPA to handle their policy layers. They can't afford a wrong policy being declared, that's why they use OPA which provides a definitive syntax for writing it. They can't afford the updates to take time, so they use OPAL for real-time synchronization. I hope you learnt something new today. Here are the links to my sources: - `OPA` - https://www.openpolicyagent.org/ - `OPAL` - https://opal.ac/ > End Note: If you check out my profile, this is my first-ever post. So please let me know how I did, and how I can improve in future. Thanks! --- ## Mastering npm: A Comprehensive Guide to Package Management **Slug:** mastering-npm-a-comprehensive-guide-to-package-management Ah, npm – the Node Package Manager. For web developers, it's like that quirky old friend who's simultaneously invaluable and infuriating. Whether you're a newbie fumbling through your first `npm install` or a seasoned dev who can recite package versions in your sleep, npm is an inescapable part of the modern JavaScript ecosystem. I've been on quite the journey with npm, from my early days of copy-pasting commands I barely understood, to now, where I can confidently say I've tamed this beast (most days, anyway). So, grab your favourite caffeinated beverage, and let's dive into the wild world of npm! ## Why Do We Even Need npm? ![The real fullform of NPM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/34pjfktpzzia7o3qy7w3.png) Picture this: You're building a web app, and you need a date picker. Sure, you _could_ write one from scratch, accounting for leap years, time zones, and all those delightful edge cases. Or... you could type `npm install moment` and have a battle-tested solution at your fingertips in seconds. That's the magic of npm. It's like having access to a vast library of code, written and maintained by developers worldwide. Need routing? Authentication? A library to validate email addresses? There's probably an npm package for that. But npm isn't just about installing packages. It's a powerful tool for: 1. **Managing Dependencies**: Keep track of what your project needs and which versions. 2. **Script Running**: Standardize commands across your team (ever seen `npm run build`?). 3. **Version Control**: Ensure everyone on your team is using the same package versions. 4. **Publishing**: Share your own code with the world (or just your team). In essence, npm is the glue that holds the JavaScript ecosystem together. It allows us to stand on the shoulders of giants and build amazing things without reinventing the wheel every time. ## But why just NPM? ![NPM vs The competition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ajl9dfkpp5uiyfc8wjlm.png) Of course NPM isn't alone, it has its own family! Sadly it isn't the most loved... but still, it's the good ol' reliable! If you want to be called a 10xengineer, you should probably switch to the alternatives. And the contenders are: | | Pros | Cons | | ---- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- | | npm | • Default for Node.js • Massive package ecosystem | • Historically slower than alternatives • node_modules can get large | | Yarn | • Faster installation • Offline mode | • Another tool to learn • Occasional compatibility issues with npm | | pnpm | • Efficient disk space usage • Lightning-fast installations | • Different node_modules structure • Less mainstream adoption | | Bun | • Blazing fast performance • All-in-one solution: runtime, transpiler, bundler | • Still in development • Limited ecosystem compared to npm | In contrast to popular belief, a 10x engineer like me is not using the freshly baked (pun intended) technology like bun! I still stick to pnpm. Why is that so you might ask? Well, it's a case specific to a Mac user like me, where Bun isn't very efficient with caching the files for repeated downloads. So it is less efficient for Macbook (or it was till the day I wrote this). ## But what are these files??? ![User asking PNPM why does it need to many lines for the lock file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fjeus8ks2tsjw7ldfxe1.png) At the core of every JavaScript project, regardless of the package manager, lies the `package.json` file. This crucial manifest outlines project details and dependencies in a structured JSON format: ```json { "name": "my-awesome-project", "version": "1.0.0", "dependencies": { ... }, "devDependencies": { ... }, "scripts": { ... } } ``` Complementing `package.json`, each package manager employs a unique lock file to ensure dependency consistency across environments. These files meticulously detail every dependency, including sub-dependencies and their exact versions: - npm: package-lock.json - Yarn: yarn.lock - pnpm: pnpm-lock.yaml - Bun: bun.lockb (in binary format) If you've ever peeked inside these lock files, you've likely encountered a daunting wall of text or, in Bun's case, indecipherable binary data. Don't panic! These files aren't meant for human editing. They're the domain of your chosen package manager, automatically generated and updated to keep your project's dependency ecosystem in perfect harmony. ## Surviving the Dependency Management Nightmare ![NPM Errors!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8133yerzwiilzljab5lo.png) Picture this: It's 2 AM, and you're fueled by coffee and determination, trying to resurrect an old project. Suddenly, npm throws a fit. One package is outdated. No, wait—all of them are. And oh, joy! That innocent-looking major update just turned your project into a digital dumpster fire. Welcome to dependency management hell, where "it works on my machine" goes to die. While we can't completely exorcise these demons (it's part of the JavaScript circle of life), we can at least arm ourselves with some holy water. Let's explore two powerful tools to keep your sanity intact. ## 1. npm-check-updates: The Blunt Force Approach First up is `npm-check-updates`, the sledgehammer of the update world. It doesn't care about your feelings or your project's delicate ecosystem. It has one job: update all the things. ```bash npm install -g npm-check-updates # Install globally ncu # List available updates (look before you leap) ncu -u # Update everything and pray ``` ## 2. npm-check: The Sophisticated Sibling For those who prefer a more refined approach, meet `npm-check`. It's like having a personal assistant for your dependencies, complete with a monocle and a British accent. ```bash npm install -g npm-check # Install globally npm-check # Get a detailed report of your dependency situation npm-check -u # Interactive update process, like a choose-your-own-adventure book ``` This tool doesn't just check for updates; it's also a snitch. It'll rat out those packages you installed and never used (we've all been there). Plus, it categorizes updates into patch, minor, and major groups, allowing you to update with the precision of a surgeon rather than the recklessness of a caffeinated developer at 2 AM. ## Conclusion We've ventured through the npm universe, from decoding `package.json` to escaping dependency hell. Here's your survival kit: 1. Choose your package manager wisely - npm, Yarn, or pnpm each have their strengths. 2. Treat your `package.json` and lock files with respect - they're the backbone of your project. 3. Use tools like npm-check-updates and npm-check to keep dependencies in check. 4. Update regularly, but cautiously. Always read changelogs and run tests. 5. Remember, even seasoned devs sometimes get lost in dependency hell - you're not alone. In the ever-changing JavaScript landscape, managing packages is more art than science. Stay curious, update wisely, and may your builds always be successful! P.S. When all else fails, there's always `rm -rf node_modules && npm install`. It's the "turn it off and on again" of the npm world! --- ## From Kubernetes Chaos to Calm: A Cyclops Adventure **Slug:** from-kubernetes-chaos-to-calm-a-cyclops-adventure Hey there, fellow coders! 👋 Ever felt like managing Kubernetes clusters was about as fun as herding cats? Well, buckle up, because we're about to dive into the world of Cyclops - the tool that promises to make Kubernetes management a walk in the park. (Spoiler alert: It actually does!) ## What's This Cyclops Thing Anyway? ![Cyclops Introduction Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eq4a48v1nu6kp4lysc6q.png) Cyclops is like that cool friend who always knows how to simplify complex stuff. It's a tool for managing Kubernetes clusters with a fancy GUI that even your non-tech-savvy cousin could probably figure out. (No offence to your cousin, of course.) Imagine Kubernetes as a massive, tangled ball of yarn, and Cyclops as the patient cat that somehow helps you unravel it without getting caught in a fur-ball of confusion. It's designed for developers, system admins, and DevOps pros who'd rather not spend their days deciphering cryptic YAML files. (Because let's face it, life's too short for that much indentation.) ## Getting Started: The "Fun" Part ![Cyclops Coding Introduction Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s6tybhm9ghnlk9b33xs6.png) ### Step 1: Get Yourself a Kubernetes Cluster First things first, you need a Kubernetes cluster. If you don't have one lying around (who does?), Minikube is your new best friend. It's like having a mini Kubernetes playground right on your laptop. Perfect for when you want to feel like a DevOps god without the fear of breaking production. Here's how to start Minikube: ```bash minikube start ``` If you're feeling fancy, you can even specify the driver: ```bash minikube start --driver=docker ``` ### Step 2: Install Cyclops Now comes the "fun" part - installing Cyclops on your cluster. Follow their installation guide, and pray to the tech gods that everything goes smoothly. (Spoiler: It probably will, but where's the drama in that?) Here's a snippet to install Cyclops using Helm: ```bash helm repo add cyclops https://cyclops-ui.github.io/helm-charts/ helm repo update helm install cyclops cyclops/cyclops ``` ### Step 3: Containerize Your App Got an app? Great! No app? Well, time to whip one up faster than you can say "Docker." Remember, it needs a UI (even if it's just a "Hello, World!" page), and it should play nice with localhost:8881. Here's a simple Dockerfile for a basic Node.js app: ```dockerfile FROM node:14 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8881 CMD [ "node", "server.js" ] ``` Now, containerize it and push it to Docker Hub. It's like gift-wrapping your app, but instead of paper and ribbons, you're using layers of filesystem and environmental variables. Festive! ```bash docker build -t yourusername/your-awesome-app:v1 . docker push yourusername/your-awesome-app:v1 ``` ### Step 4: Deploy with Cyclops Fire up the Cyclops GUI on localhost. Navigate to the modules section like you're Christopher Columbus discovering the New World of Kubernetes management. Click "Add new module" and select the demo template. You'll see a screen that looks like it's straight out of a sci-fi movie, but don't panic! This is where the magic happens. If you're more of a CLI person (we don't judge), you can use kubectl to apply your Cyclops-generated YAML: ```bash kubectl apply -f your-cyclops-generated-config.yaml ``` ### Step 5: Port Forwarding (Because Why Make Things Simple?) After your service is deployed, it's time for some command-line fun. Run this magical incantation: ```bash kubectl port-forward svc/your-awesome-app 8881:80 ``` Replace `your-awesome-app` with, well, your awesome app's name. If it doesn't work, don't freak out. Cyclops likes to keep you on your toes by defaulting to port 80. Just change it to another port and pretend that's what you meant to do all along. ## The Grand Finale Voilà! Your app should now be running on localhost:8881 (or whatever port you ended up using). Take a moment to bask in the glory of your achievement. You've just deployed an app using Cyclops, and the Kubernetes gods are smiling upon you. To check if your app is really running (and not just hiding from you), try: ```bash curl http://localhost:8881 ``` If you see your app's content, congratulations! If not, well, welcome to the wonderful world of debugging Kubernetes deployments! ## Contributing to Cyclops: My Adventure with GitHub Issues ![Contributing to Cyclops Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j1o9lxijj0vhrrvs922m.png) Now that we've gone through the basics of using Cyclops, let's talk about something even cooler - contributing to the project itself! As part of this quest, I had the opportunity to work on a real GitHub issue for Cyclops. Let me tell you, it's as exciting as finding an unattended snack in the office kitchen! ### The Issue: Taming the Impatient User (Including Myself) The issue I tackled was #423: "Disable all actions while the template is loading". Sounds thrilling, right? Well, hold onto your keyboards, because it actually is pretty important! Here's the gist: When adding a new module in Cyclops, you choose a template. But templates, like your colleague's never-ending status updates, can take a while to load. The problem was that impatient users (guilty as charged) could click around and potentially cause chaos while the template was still loading. ### The Solution: Adding a Digital Straitjacket To solve this, we needed to add some restrictions - a digital straitjacket, if you will. Here's what I did: 1. Disabled the 'save' button while loading 2. Disabled the 'load values from file' button 3. Prevented users from changing the template mid-load Here's a simplified snippet of the changes I made: ```typescript const [isLoading, setIsLoading] = useState(false); const handleTemplateLoad = async () => { setIsLoading(true); try { await loadTemplate(); } finally { setIsLoading(false); } }; return ( Load Template Save Load Values from File {/* Template options */} ); ``` The key was using a loading state to control the UI elements. It's like putting a "Do Not Disturb" sign on your desk but for web components. ### How You Can Contribute Think you can't contribute because you're not a Cyclops expert? Nonsense! Here's a quick guide to getting started: 1. **Find an Issue**: Browse the [Cyclops GitHub Issues](https://github.com/cyclops-ui/cyclops/issues) page. Look for "good first issue" or "help wanted" labels - they're the low-hanging fruit of the coding world. 2. **Dive In**: Set up your environment, comment on an issue to claim it (no one likes a code stealer), and start coding. Don't forget to test your changes - untested code is like a mystery flavour jellybean. 3. **Submit and Iterate**: Send in your pull request, describing your changes like you're selling them on a late-night infomercial. Be ready for feedback - it's all part of the open-source tango. Contributing isn't just about the code - it's about learning, connecting with fellow developers, and getting that warm, fuzzy feeling of making the world a tiny bit better. Plus, it looks pretty snazzy on your resume. So why wait? Dive in and start your Cyclops adventure today! Remember, in the world of open-source, we're all Cyclops - focused on one goal at a time, but with a vision that spans the entire Kubernetes landscape. Happy coding! ## What's Next? Now that you've dipped your toes into the Cyclops pool, why not dive deeper? Explore more of Cyclops' tools, poke around your Kubernetes cluster, break things (in a non-production environment, of course), and learn from the chaos. And hey, if all else fails, you can always fall back on the age-old tech solution: turn it off and on again. Works every time, 60% of the time. ```bash minikube stop minikube start ``` Happy Cycloptic adventures, and may your containers always be running! --- _P.S. If you found this guide helpful, consider giving Cyclops some love by starring their [GitHub repo](https://github.com/cyclops-ui/cyclops). After all, they're the reason we're not all crying into our keyboards trying to manage Kubernetes the old-fashioned way._ --- ## Maven Conditional Dependencies with Profiles **Slug:** conditional-dependency-management-using-maven-profiles Maven profiles let you include dependencies only when a particular environment, platform, or build condition is active. Put the conditional dependency inside a ``, give the profile an ID, and select it with `mvn package -P`. For example, this activates a production-only dependency: ```xml production ch.qos.logback logback-classic 1.2.6 ``` Run it with `mvn clean package -Pproduction`. The rest of this guide builds a complete project and covers command-line, settings, environment, and operating-system activation. ## How Maven profiles manage conditional dependencies A Maven profile is a set of parameters that alters the default build. Profiles let you define different dependencies and configuration for development, testing, production, target platforms, or other build requirements. **Key Concepts:** - **Profile Activation:** Activations can be of several types depending on the option keys like command-line, Maven option settings, system environment variables, and system preferences. - **Dependency Management:** It is possible to use profiles to specify which dependencies should be included and which ones should be excluded based on the currently active profile. - **Build Customization:** With profiles, you can control how your build process is set up in aspects like plug-in settings, resources being filtered etc. ## Setting Up a Maven Project with Profiles Let's walk through the process of setting up a Maven project that uses profiles for conditional dependency management. ### Step 1: Create a New Maven Project First, let's create a new Maven project using the command line: ```bash mvn archetype:generate -DgroupId=com.example -DartifactId=profile-demo -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false ``` **Output:** This command will create a new Maven project with a basic structure. Here is what the output should look like: ![Output of the create new Maven project command](https://media.geeksforgeeks.org/wp-content/uploads/20240804001437/screenshot.png) Now, let's take a look at the directory structure that Maven has created for us: ![Directory Structure](https://media.geeksforgeeks.org/wp-content/uploads/20240806105510/mvn-pro.png) ### Step 2: Configure pom.xml Now, let's modify the pom.xml file to include profiles. We'll create two profiles: development and production, each with its own set of dependencies. ```xml 4.0.0 com.example profile-demo 1.0-SNAPSHOT 1.8 1.8 junit junit 4.13.2 test development true org.slf4j slf4j-simple 1.7.32 production ch.qos.logback logback-classic 1.2.6 ``` In this configuration: - We have a common dependency (JUnit) that's always included. - The development profile includes the slf4j-simple logger. - The production profile includes the logback-classic logger. - The development profile is set as the default profile. ### Step 3: Create Java Classes Let's create a simple Java class that uses the logger: ```java package com.example; public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { logger.info("Hello, Maven Profiles!"); } } ``` ### Step 4: Build the Project Now that we have set up our project with profiles, let's see how to use them. #### Building with the Default Profile To build the project with the default profile (development in our case), simply run: ```bash mvn clean package ``` This will include the slf4j-simple logger in the build. #### Building with a Specific Profile To build the project with the production profile, use the -P flag: ```bash mvn clean package -Pproduction ``` This will include the logback-classic logger in the build instead. ### Advanced Profile Usage Profiles can be activated in various ways: 1. **Command Line:** As shown above, using `-P`. For example, To activate the 'development' profile: ```bash mvn clean package -Pdevelopment ``` 2. **Maven Settings:** In the `settings.xml` file: ```xml production ``` 3. **Environment Variable:** In the `pom.xml`: ```xml env prod ``` Then activate using: `mvn clean package -Denv=prod` 4. **OS Settings:** Activate based on the operating system: ```xml Windows 10 Windows amd64 10.0 ``` #### Resource Filtering: Profiles can also be used for resource filtering. For example: ```xml development src/main/resources true application-dev.properties ``` This will only include the `application-dev.properties` file when the development profile is active. ### Conclusion Maven Profiles provide a powerful way to manage conditional dependencies and configurations in your Java projects. By using profiles, you can easily switch between different build configurations for various environments or conditions. This flexibility allows for more maintainable and adaptable projects, especially when dealing with complex build requirements or multiple deployment scenarios. ### References: - GeeksforGeeks: [Apache Maven](https://www.geeksforgeeks.org/apache-maven/) - GeeksforGeeks: [Maven Lifecycle and Basic Maven Commands](https://www.geeksforgeeks.org/maven-lifecycle-and-basic-maven-commands/) --- ## Neon T3 Starter Kit: Supercharging Web Development with Serverless Postgres **Slug:** neon-t3-starter-kit-supercharging-web-development-with-serverless-postgres The Neon T3 Starter Kit is the ultimate open-source starter kit assembled around the T3 stack that uses Neon’s serverless Postgres database. This kit can easily be described as a game changer, offering developers a solid base from which they can build engaging, modern web applications with relative ease. Our starter kit leverages the following technologies: - Next.js framework for server-side rendering capabilities and a Full-stack environment. - TypeScript for type-safe programming. - Tailwind CSS for rapid frontend development/\. - tRPC to provide end-to-end Typesafe APIs - Prisma to provide ORM in database operations. - Neon to provide a fully scalable and easy-to-set-up server-less Postgres database. (Not to mention completely free!) > Note there were also other tools I used such as Shadcn, Magicpin, Lucide Icons, etc. A detailed list can be found on the GitHub Repository. With this powerful combination, developers can dedicate more time to creating features instead of worrying about infrastructural issues. ## Link to Kit Link to live demo site: https://neon-t3-starter-kit.vercel.app/ You can find the Neon T3 Starter Kit on GitHub: https://github.com/ChiragAgg5k/neon-t3-starter-kit This repository also contains a well-structured README that guides the user on how to use the said starter kit. We have also documented well how to set up the environment, configure Neon database, and deploy on sites like Vercel. ## My Journey Selecting the T3 stack as a base for this Start kit was quite an obvious decision. The T3 stack created by Theo Browne is quite simple, type-safe, and is quite friendly to developers. In doing so, it serves as a reliable framework that allows for the development of scalable web applications. Bringing Neon’s serverless Postgres into this stack was a fun task. I was glad to learn a lot about the server-less architecture of databases and how to make my applications faster and more scalable than before. Learning how to integrate Prisma with Neon’s Postgres instance was learning in itself because it demonstrated how other modern ORMs can easily integrate with modern databases. Throughout this journey, I gained valuable insights into: 1. Using optimal database connections in a serverless environment 2. Utilising Neon’s branching for effective development workflows 3. Prisma and tRPC Type-Safe Database Operations 4. Challenges of maintaining both performance and developer experience in a full-stack application Developing this starter kit has been a wonderful learning process. The developer’s tools have been improved through the collaboration of other developers to ensure that documentation is well-provided and easily accessible. This Neon T3 Starter Kit should assist developers who are now facing issues with finding the right, maintainable, and enjoyable project stack! ![Landing Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pgmusk7stzqsbff46eta.png) --- ## How to Register Users in Django REST Framework? **Slug:** how-to-register-users-in-django-rest-framework **Django REST Framework (DRF)** is a powerful and flexible toolkit for building Web APIs. One of the most common requirements in web applications is user registration. In this article, we'll explore how to implement user registration using [Django REST Framework](https://www.geeksforgeeks.org/django-rest-api-crud-with-drf/). ## Setting Up Django REST Framework Before we dive into the Django REST Framework, let's set up a new Django project. Follow these steps: 1. First, ensure we have Python installed on our system. We can download Python from [python.org](https://www.python.org/). 2. Install Django and Django REST Framework using `pip`: ```bash pip install django djangorestframework ``` 3. Create a new Django project: ```bash django-admin startproject django_rest_demo cd django_rest_demo ``` 4. Create a new Django app: ```bash python manage.py startapp user_auth ``` 5. Open `django_rest_demo/settings.py` and add the `user_auth` and `rest_framework` to `INSTALLED_APPS`: ```py INSTALLED_APPS = [ # ... 'rest_framework', # Token authentication 'rest_framework.authtoken', 'user_auth', ] ``` 6. Run initial migrations: ```bash python manage.py migrate ``` Now that we have our Django project set up, let's configure Django REST Framework: 1. In the `django_rest_demo/settings.py`, we can add any DRF-specific settings. For example: ```py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], } ``` 2. If we're using token authentication, make sure we've added `rest_framework.authtoken` to `INSTALLED_APPS` as shown in **step 5** above. Here's how our project structure might look: ```bash django_rest_demo/ │ ├── user_auth/ │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── views.py │ └── urls.py │ ├── django_rest_demo/ │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ └── manage.py ``` ## Creating a Registration Serializer Serializers in DRF allow complex data, such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into **JSON**, **XML** or other content types. They also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data. Let's create a serializer for user registration: In the `user_auth/serializers.py`: ```py from rest_framework import serializers from django.contrib.auth.models import User class UserRegistrationSerializer(serializers.ModelSerializer): password2 = serializers.CharField(style={'input_type': 'password'}, write_only=True) class Meta: model = User fields = ['username', 'email', 'password', 'password2'] extra_kwargs = { 'password': {'write_only': True} } def validate(self, attrs): if attrs['password'] != attrs['password2']: raise serializers.ValidationError({"password": "Password fields didn't match."}) return attrs def create(self, validated_data): user = User.objects.create_user( username=validated_data['username'], email=validated_data['email'], password=validated_data['password'] ) return user ``` This serializer extends ModelSerializer and uses the User model. It includes a password confirmation field and custom validation to ensure the passwords match. ## Handling User Registration with Views Now that we have our serializer, let's create a view to handle the registration process. In the `user_auth/views.py`: ```py from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from .serializers import UserRegistrationSerializer class UserRegistrationView(APIView): def post(self, request): serializer = UserRegistrationSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response({ "message": "User registered successfully" }, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) ``` This view uses the UserRegistrationSerializer to validate the incoming data and create a new user if the data is valid. ### Example Code To complete our code, let's set up the URLs to make our registration view accessible. In our `user_auth/urls.py` (create this file if it doesn't exist): ```py from django.urls import path from .views import UserRegistrationView urlpatterns = [ path('register/', UserRegistrationView.as_view(), name='register'), ] ``` Then, in the project's `django_rest_demo/urls.py`: ```py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('user_auth.urls')), ] ``` ## Testing the Registration Endpoint We can now run our Django development server: ```bash python manage.py runserver ``` To test the registration endpoint, we can use tools like curl, Postman, or even the Django REST Framework browsable API. Here's an example using curl: ```bash curl -X POST http://localhost:8000/api/register/ -H "Content-Type: application/json" -d '{"username":"newuser", "email":"newuser@example.com", "password":"securepassword", "password2":"securepassword"}' ``` If successful, we should receive a response indicating that the user was registered successfully. ![api-call-success](https://media.geeksforgeeks.org/wp-content/uploads/20241001015217/api-call-success.png) ## Conclusion In this article, we've covered how to set up a Django project named "django-rest-demo" from scratch and implement user registration using Django REST Framework in an app called "user_auth". We created a custom serializer to handle the registration data, including password confirmation and validation. We then created a view to process the registration and return appropriate responses. This implementation provides a solid foundation for user registration in our DRF project. Remember to add appropriate permissions and authentication to our views as needed, and consider adding features like email verification for a more robust registration process. --- ## My Hacktoberfest 2024 Experience with Cal Buddy, Your Smart Calendar Assistant **Slug:** my-hacktoberfest-2024-experience-with-cal-buddy-your-smart-calendar-assistant _This is was submission for the [2024 Hacktoberfest Writing challenge](https://dev.to/challenges/hacktoberfest): Maintainer Experience_ Oh boy, what a ride! 🎢 As Hacktoberfest 2024 comes to a close, I can't help but grin thinking about the amazing journey we've had with Cal Buddy, the AI-powered calendar assistant that's been turning heads and stealing hearts in the open-source community. ## From Quest to Conquest! Here's a fun origin story for you: Cal Buddy wasn't just born out of a random shower thought—it actually started as my submission for a Quira quest! Quira (https://quira.dev) is a developer platform that hosts technical challenges where developers can showcase their skills and build innovative projects. Their quests are tech challenges that push developers to build something cool with specific technologies. My mission? Create something amazing using CopilotKit. Talk about a quest that turned into an adventure! ![Quira quest page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ckf8jv3mmhp4ypuyzgi.png) Just as I was scheduling this task of working on the submission on my Google calendar, I thought, "Hey, what if we made calendars actually fun to use?" And thus, Cal Buddy was born—a smart calendar assistant that proves even time management can have a personality! ## The Little Calendar That Could You know what's better than a regular calendar? A calendar with a brain! That's exactly what Cal Buddy is—a smart calendar assistant that helps you manage your schedule with the power of AI. Think of it as having a super-organized friend who never forgets a meeting and always knows exactly when to schedule that coffee break you desperately need. ## The Numbers Don't Lie (They're Just Really Excited!) Here's something that made us do a happy dance: during Hacktoberfest, Cal Buddy snagged an impressive 26 stars! ⭐ That's like getting 26 high-fives from developers around the world saying, "Hey, this is pretty cool!" But wait, it gets better—we had 6 awesome contributors join our calendar-revolutionising crusade. Each one bringing their unique flavour to our code cocktail! ![Cal Buddy star history](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7h0igo4skuoawuneuh5.png) ## What Makes Cal Buddy Special? Let me tell you, this isn't your grandmother's calendar app (though she'd probably love it too!). Thanks to the power of CopilotKit, we've packed it with some seriously cool features: - 🤖 AI-powered scheduling that's smarter than a calendar has any right to be - 💬 A chat interface so friendly, you'll forget you're talking to an app - 📊 Productivity insights that make spreadsheets look like child's play - 🔗 Calendar service integrations that play nicer than kids at a birthday party ![Chat Interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o13cfyzeflnw64wc4ks8.png) ## The Open Source Spirit What really made this Hacktoberfest special was seeing how Cal Buddy evolved from a quest submission into a full-fledged open-source project. Our contributors brought thoughtful improvements and clever optimizations that transformed the project beyond our initial vision. ## Looking Ahead While 26 stars might not break any GitHub records, each one represents someone who believed in what we're building. And those 6 contributors? They're the real MVPs, turning our calendar assistant dream into reality, one commit at a time. Not bad for something that started as a quest submission, right? ## Want to Join the Calendar Revolution? The party doesn't stop with Hacktoberfest! Cal Buddy's door is always open to new contributors who want to help make scheduling smarter and more fun. Whether you're a coding wizard or just getting started, there's room for everyone in our calendar family. 🔗 [Github Repository](https://github.com/ChiragAgg5k/cal-buddy) ## The Bottom Line Hacktoberfest 2024 taught us something important: sometimes the best projects come from unexpected beginnings. What started as a Quira quest with CopilotKit turned into something that brought people together and sparked joy in the developer community. Cal Buddy might be an AI calendar assistant, but the real intelligence comes from the amazing humans who've helped build it. So here's to the quest that started it all—the stars, the contributors, and everyone who believed that calendars deserved better. Cal Buddy is just getting started, and the future is looking bright—and perfectly scheduled! 📅✨ Remember, in a world full of regular calendars, be a Cal Buddy! 🚀 --- _Got time management problems? Cal Buddy's got your back! Check us out at [cal-buddy.vercel.app](https://cal-buddy.vercel.app/)_ --- ## My Hackfrost Journey: Navigating Development Challenges with Daytona **Slug:** my-hackfrost-journey-navigating-development-challenges-with-daytona ## The Winter of Tech Innovation As the crisp winter winds swept across India, the tech community buzzed with excitement. Hackfrost, a hackathon organized by the dynamic WeMakeDevs community—founded by the renowned tech educator Kunal Kushwaha—promised to be more than just another coding competition. This 48-hour virtual event was set to challenge developers, foster innovation, and bring together a diverse group of tech enthusiasts from across the country. ![Hackfrost Cover Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5tpogusxub7v18mw2cec.png) ## The Allure of the Challenge When I first stumbled upon the hackathon announcement, it was the prize lineup that immediately caught my eye—a treasure trove that would make any developer's heart race: - Latest MacBook Pro - High-end Dell Monitor - Sleek iPad - Compact M4 Mac Mini - Premium Keychron Keyboards - An assortment of exclusive tech swag But this wasn't just a simple giveaway. The competition had a unique twist: every project needed to incorporate Kestra, an open-source orchestration tool that was gaining significant traction in the developer community. What seemed like a constraint at first quickly transformed into an exciting opportunity for innovation. ## Team Formation: A Community-Driven Approach My journey began in the vibrant corridors of Quira, an open-source community that has been my technical home for quite some time. Networking has always been more than just collecting contact information—it's about finding passionate individuals who complement your skills and share your vision. I was fortunate to connect with an incredible team: - **K Om Senapati**: A brilliant problem solver with a knack for backend architecture - **Juanita**: A frontend wizard with an eye for user experience - **Chelsea**: Our team's DevOps expert who understands the intricacies of cloud infrastructure Our diversity was our strength. Each of us brought unique perspectives and skills to the table, transforming a potential challenge into an opportunity for collaborative innovation. ## The Development Environment Dilemma Any developer who has worked on a team project knows the pain of environment setup. It's a time-consuming process typically involving: - Endless configuration scripts - Dependency version conflicts - Complex environment variable management - Hours of synchronization between team members This is where Daytona emerged as our unexpected hero. What would traditionally take days was reduced to mere minutes. The tool's ability to streamline development environments was nothing short of revolutionary for our team. ![Daytona Thumbnail](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0dylirv94hune4s3bynf.png) ### Development Container Configuration with Daytona To ensure a consistent development environment, we leveraged Daytona's powerful configuration capabilities. Here's a step-by-step guide to setting up Daytona for our project: 1. **Install Daytona**: You can find the instructions specific to your OS on their website [here](https://www.daytona.io/). 2. **Create Devcontainer Configuration** We crafted a detailed devcontainer configuration to standardize our development environment: ```json { "name": "Flow Forge Development Container", "build": { "dockerfile": "Dockerfile", "context": ".." }, "customizations": { "vscode": { "settings": { "terminal.integrated.shell.linux": "/bin/sh", "eslint.enable": true, "prettier.resolveGlobalModules": true }, "extensions": [ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-vscode.vscode-typescript-tslint" ] } } } ``` 3. **Add a Provider**: We opted for a cloud provider due to low-spec configurations on some of our teammates' systems. Specifically, due to an abundance of Azure credits, we chose Azure using: ```bash daytona provider install ``` 4. **Running the Project**: After pushing our devcontainer to the project repo, we can start the environment easily with: ```bash daytona create https://github.com/ChiragAgg5k/flow-forge --devcontainer-path=.devcontainer/devcontainer.json ``` **Feedback and Reflections** While traditional development environments often feel like navigating a maze of configurations, Daytona emerged as a breath of fresh air. Its intuitive approach to setting up development environments was nothing short of revolutionary for our team. Key Observations: - **Simplicity**: What traditionally took hours of manual configuration was reduced to a few command-line interactions. - **Consistency**: Every team member's environment was identical, eliminating the notorious "it works on my machine" syndrome. - **Flexibility**: The tool seamlessly handled different development setups, from local machines to cloud-based environments. ### Cloud-Powered Development Given my modest MacBook's specifications, we decided to leverage cloud computing. Daytona's seamless Azure integration was a game-changer. With just a few clicks, I added Azure as our cloud provider, and suddenly, our entire development ecosystem was accessible, consistent, and performant. The magic of Daytona wasn't just in its simplicity, but in its ability to democratize development environments. Teammates with varying hardware could now collaborate effortlessly, breaking down technological barriers. ## Architecting the GitHub Workflow Manager Our project aimed to solve a real-world problem: simplifying GitHub workflow management using Kestra's powerful orchestration capabilities. ### Technical Architecture - **Frontend**: Next.js, chosen for its server-side rendering and robust ecosystem - **Orchestration**: Kestra, running on an Azure VM - **Authentication & Backend**: Appwrite, providing a flexible Backend-as-a-Service solution We implemented basic authentication to interact with the Kestra instance, allowing users to define, monitor, and execute complex workflows with unprecedented ease. ### Key Features - Seamless GitHub workflow creation - Real-time workflow status tracking - Customizable workflow templates - Secure authentication mechanisms ![Kestra Worfklow Thumbnail](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdblxgqij2hp93upccg9.png) ## Beyond the Competition While the prize table remained unclaimed by our team, the true value of Hackfrost transcended material rewards. We had: - Learned cutting-edge technologies - Practiced collaborative development - Solved a tangible industry problem - Strengthened our professional network **Daytona's Impact**: The development tool didn't just simplify our setup—it transformed how we think about collaborative coding. ## Final Thoughts Tech competitions are rarely about winning. They're about growth, learning, and pushing technological boundaries. Our Hackfrost journey exemplified this philosophy—a testament to the power of community, innovation, and the right tools. To Daytona, Kestra, and the entire WeMakeDevs community: Thank you for creating spaces where innovation thrives. --- _Check out the live project [here](https://flow-forge-iota.vercel.app/)_ _For more information about me, checkout my [Portfolio](https://www.chiragaggarwal.tech/)_ --- ## MVC vs MVP vs MVVM: Key Differences **Slug:** architecture-patterns-for-beginners-mvc-mvp-and-mvvm MVC, MVP, and MVVM all separate application data from the user interface, but they put presentation logic in different places. **MVC uses a controller, MVP routes UI work through a presenter, and MVVM connects the view to a view model through data binding.** Choose MVC when simplicity and framework support matter most, MVP when you need explicit separation and easy unit testing, and MVVM when a reactive UI benefits from data binding. The comparison below explains how their data flow, coupling, testability, and ideal use cases differ. --- ## Architectural patterns Whenever we are talking about architectural patterns in software design, the first ones to top the list include architectures like client-server, layered, monolithic, microkernel, even-driven, etc. These patterns are concerned with overall system architecture, including multiple applications, services, servers, etc. However, MVP, MVC, and MVVM focus on organizing code within a single application by separating data, user interface, and logic. These are a subset of architecture patterns that focus on the overall system. ![Client-Server Architecture vs MVC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsnn1y32wzy0gz5bnl4z.png) --- ## MVC, MVP, and MVVM For the sake of keeping the blog readable and not exceeding the word length, we will focus on just the architectural patterns that organize the code within a single application, namely: 1. Model-View-Controller 2. Model-View-Presenter 3. Model-View-ViewModel Clearly, all three models have two components fixed, i.e. the Model and the View. So let's first discuss them in detail before coming to each of the architecture. ### Model The model consists of all the code that is related to **data** present in the software. It's the layer for communication of the database and network layers with the rest of the application. Its main responsibilities include: 1. Handle data and business logic. 2. Encapsulate the application's data and the rules governing access to that data. 3. Handling data structures. 4. Performing CRUD (Create, Read, Update, and Delete) operations on data. ![Functioning of Model layer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yacza08pqkbmgb5ae9cf.png) ### View View is pretty much the front end of your application or everything that the user will be able to see and interact with. It's also known as the User Interface (UI) of your application. Its responsibilities include: 1. Handle non-business logic and purely presentational logic. 2. Present the data provided by other layers to the user. 3. Receive user input and forward it to other layers. 4. May or may communicate directly with the Model layer. ## Model-View-Controller (MVC) Architecture ![Model-View-Controller Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t34ojxv1pyae6kogh33t.png) Now that we have an understanding of what Model and View layers do, let's take a look at individual architectural patterns. Starting with MVC, it uses a **Controller** layer that communicates with both the Model and View layers. It's main responsibilities of the controller include: 1. Manipulating data through the Model layer. 2. Receive instructions, aka the UI, from the View layer. 3. Update the View with changes defined due to control logic. Here, although the View layer cannot directly interact with the Model layer, it can however receive updates based on changes in the data. Hence all three layers are connected to each other in some form, with the controller being the main component. ## Model-View-Presenter (MVP) Architecture ![Model-View-Presenter Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hw9i30c4jrun2ryzxup8.png) Here, the Presenter layer assumes the functionality of the "middle-man" between the Model and View layers and handles all the communication between them. There is no communication at all between the Model and View layers directly. Its responsibilities include: 1. Update the UI or the View layer based on user actions. 2. Update the data or Model layer based on code logic. 3. Handle much of the business logic that would be otherwise handled in the controller in MVC architecture. ## Model-View-ViewModel (MVVM) Architecture ![Model-View-ViewModel Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r7ik37i57yt9vbx70z7c.png) This architecture at first glance is almost identical to MVP architecture. But there are some key differences between them: 1. Multiple views can be mapped to a single ViewModel layer. 2. It uses data binding between the ViewModel layer and the View layer, making it more event-driven. 3. There is no concept of User Interface in this architecture. The View layer represents the actions of the user, not the interface. ## MVC vs MVP vs MVVM: side-by-side comparison | Aspect | MVC | MVP | MVVM | | -------------------------- | ----------------------------------- | --------------------------------------- | ---------------------------------------------- | | Full Name | Model-View-Controller | Model-View-Presenter | Model-View-ViewModel | | Separation of Concerns | Basic | Better | Best | | Data Flow | Two-way | One-way | One-way with data binding | | View-Logic Relationship | Many-to-one | One-to-one | Many-to-one | | Testability | Hard | Good | Best | | Maintenance | Hard | Easy | Easy | | Learning Curve | Easy | Easy | Harder | | Performance | Can be slower due to tight coupling | Better performance with looser coupling | Smooth performance, especially for complex UIs | | UI Updates | Controller updates View | Presenter updates View | ViewModel updates View through data binding | | Dependency on UI Framework | High | Low | Low or no dependency | | Scalability | Suitable for small-scale projects | Good for simple and complex projects | Ideal for large, data-heavy apps | But which is the most popular you might ask? All of them are equally popular architectures being used according to the company's respective requirements for a product. Some companies adopting these different architectures are: 1. MVC: StackOverflow, GoDaddy, Visual Studio website Dell 2. MVP: Google (for some Android apps) 3. MVVM: Apple (for some iOS apps using SwiftUI), Angular framework, Vue.js framework Also, many companies use a mix of these architectures depending on the specific needs of each project or product. The choice of architecture often depends on factors such as the complexity of the application, the development team's expertise, and the specific requirements of the project. --- ## Conclusion This article covered the basics of architectural patterns, from how overall architecture is designed to how a single application can be further divided into three components for better management and scalability. - MVC, with its straightforward approach, remains popular for web applications. - MVP builds upon MVC's foundation, offering improved testability and a cleaner separation of concerns. - MVVM, the most recent of the three, has gained significant traction in modern application development. There is no clear winner between them and each pattern offers unique advantages and is suited to different projects and development scenarios. As the software development landscape continues to evolve, we may see further refinements of these patterns or the emergence of new architectures altogether. Want to learn more about the architectural patterns discussed? Here are some references I found helpful: 1. https://www.geeksforgeeks.org/android-architecture-patterns/ 2. https://www.masaischool.com/blog/comparing-software-architecture-patterns/ 3. https://www.apptension.com/blog-posts/mvc-vs-mvvm-vs-mvp --- ## Writing Event-Driven Serverless Code to Build Scalable Applications **Slug:** writing-event-driven-serverless-code-to-build-scalable-applications Serverless isn't just trendy—it's rewriting how software scales. Netflix streams billions of hours without servers. Coca-Cola automates workflows without infrastructure. Figma and T-Mobile ditch downtime. What do they know that you don't? The secret? Event-driven serverless code. It's the backbone of apps that scale instantly, cut costs, and survive traffic spikes. No servers. No guesswork. Just code that reacts. This isn't hype—it's a blueprint. Ready to build smarter? Let's break down how event-driven serverless turns scalability from a challenge into a reflex. --- ## **Brief Intro to Serverless** > Spoiler alert: servers are still there. There are multiple definitions for this term online, often filled with complex jargon. The best way I like to define it is: _A "fashion" of deploying your code where YOU don't have to think about the servers running your code._ Let's take an example: ![Serverless Deployment Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/884rktyfom0qj4zwatzm.png) Take Bob. He built _mytrashcode.com_ but panicked at "server setup." He's a developer, not a sysadmin. Instead, he uploaded his code to a cloud provider. They handled security, scaling, and traffic—his site went live. No late-night server meltdowns. No panic during traffic surges. Done. --- ## **Why Can't I Manage My Own Servers?** Managing your own servers usually takes one of two paths. You either run physical hardware—like turning an old laptop into a DIY server—or rent a Virtual Private Server (VPS) from providers like DigitalOcean Droplets, Azure VMs, or AWS Lightsail. These fall under IaaS (Infrastructure as a Service), where the cloud company provides the bare-metal infrastructure, but the rest—updates, scaling, security—is entirely up to you. Does this mean self-managing servers is impossible? Not at all. Plenty of teams still do it. But managing your own servers comes with a lot of... challenges, including: 1. Knowing how to manage **infrastructure**/hardware. 2. Setting up **auto-scaling** and downscaling. 3. Periodically applying **system patches** and updates to avoid exposing vulnerabilities. 4. Configuring proxies, SSL certificate generation, **network settings**, etc. --- ## **Dividing Your Code into Functions** Serverless code doesn't need to be monolithic, i.e., all code doesn't need to be in the same place. It can be a collection of bite-sized, event-triggered functions. A **Function** is nothing but a set of code that performs a specific task. When writing your entire code serverless, you'll find that you can divide your code into various functions, each handling a specific part of your application. Let's understand this more deeply with an example: ![Functions Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rc9zdithhna5sjs2gup4.png) When Bob first logs into _mytrashcode.com_ as a new user, the system triggers a "send welcome email" function before redirecting him. Subsequent logins bypass this function entirely, routing him straight to the dashboard. This separation serves a critical purpose—while 99% of users interact solely with the dashboard, isolating secondary functions (like email triggers) enables independent scaling. Though trivial in this example, the cost implications compound dramatically at scale. Each decoupled function operates on its own resource allocation curve—high-frequency features like dashboard access demand consistent infrastructure, while one-time actions (welcome emails) can scale down during inactive periods. This modular approach prevents overprovisioning for rarely triggered events, even before considering complex systems with hundreds of interdependent functions. --- ## **Where to Deploy???** Okay, so just a quick recap—we now know: 1. Deploying serverless is great! 2. Dividing your code into functions is modular and scalable. 3. Functions can be triggered by events. So, where do you deploy this architecture? Leading platforms like AWS Lambda, Azure Functions, and Google Cloud Functions support it, but we'll focus on Appwrite Functions. ![Function Deployment Options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9sqdus26z0ygxclj0ms.png) Appwrite, an open-source Backend-as-a-Service (BaaS), bundles authentication, databases, storage, and serverless functions into a single toolkit. This tight integration streamlines deployment—instead of managing fragmented cloud services, Appwrite centralizes backend logic, letting you deploy event-driven functions with minimal overhead. For developers prioritizing simplicity without sacrificing scalability, this unified approach reduces operational friction significantly. So, let's deploy our function! --- ## **Deploying Your First Function** Before writing code, set up your backend on Appwrite: 1. Go to [appwrite.io](https://appwrite.io/) and register or log in. 2. Create an organization (if new). 3. Create a new project. 4. Copy your **Project ID** for later use. Now, let's simulate a server-side project using the `node-appwrite` package: - Create a project directory: ```bash mkdir my-project cd my-project ``` - Install the [Appwrite CLI](https://appwrite.io/docs/tooling/command-line/installation) and initialize your project: ```bash npm init -y appwrite init ``` - Install dependencies: ```bash npm install dotenv node-appwrite ``` - Create your function using the Appwrite CLI: ```bash appwrite init function ``` For the runtime, I selected **Node 20**, but you can choose any runtime. - Write your main function in `src/main.js`: ```js dotenv.config(); const client = new Client(); client.setEndpoint("https://cloud.appwrite.io/v1"); client.setProject(process.env.PROJECT_ID); const users = new Users(client); const account = new Account(client); const main = async () => { await account.create("test-user", "test@test.com", "test@123", "test"); const session = await account.createEmailPasswordSession( "test@test.com", "test@123", ); console.log(session); }; main(); ``` - Add a `start` script in `package.json` to run `node src/main.js`. - Create a `.env` file with the required environment variables. This function simulates a new user creation and login, logging the session details. > **Note:** Replace the email IDs with actual emails to receive the email. ![Functions Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pa9qh5cp756w6a5jpjrt.png) Now, let's set up the function logic. Navigate to `functions/your-function` where your function resides. For this demo, we'll use [Resend](https://resend.com) to send emails: - Install the `resend` package: ```bash npm install resend ``` - Update `src/main.js` with this code: ```js // https://appwrite.io/docs/advanced/platform/events export default async ({ res, req, log }) => { const resend = new Resend(process.env.RESEND_API_KEY); await resend.emails.send({ from: "hello@yourdomain.com", to: req.body.email, subject: "Hello!", text: "Hi, its nice to meet you!", }); log("Email sent successfully"); return res.json({ success: true, message: "Email sent successfully", }); }; ``` You need to set up an account on Resend to get the API Key. Resend also requires you to connect to your own domain to send emails. You can read more about it on the [Resend docs](https://resend.com/docs/introduction). Now, let's push the created function to the console using: ```bash appwrite push functions ``` The final step is to set up the event that connects the two pieces of code together using the `users.*.create` event: 1. Go to the Appwrite console and navigate to your created project. 2. Navigate to the Functions tab. 3. You should see your newly created function there—click on it. 4. Go to its settings and under the Events section. 5. Add a new event to trigger this function: `users.*.create`. ![Appwrite Console showing Events](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sb7etulqcgb31sx5at2t.png) And... voila! Your program is done. If everything is set up correctly, running your main script should send the newly created user an invitation email. Try it using: ```bash npm run start ``` --- ## **Conclusion** In conclusion, serverless architecture is more than just a passing trend—it's a transformative approach to building and scaling modern applications. Platforms like Appwrite further simplify the process, offering a unified backend solution that integrates seamlessly with serverless functions. Whether you're a solo developer like Bob or part of a larger team, adopting serverless can turn scalability from a daunting challenge into an effortless reflex. ![Conclusion Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jnuiov1eikg0u26hgpyt.png) --- _Thanks for reading!_ _You can also connect with me here: [https://www.chiragaggarwal.tech/](https://www.chiragaggarwal.tech/)_ --- ## Debugging with Source Maps: A Comprehensive Guide **Slug:** debugging-with-source-maps-a-comprehensive-guide We all have been there, working on a new feature for 10+ hours straight, and everything is going well. You build your project and push the code to production. And boom, a new production error alert! Everyone in your team - whether at a company or a hackathon - starts to look for someone to blame. Found it; it was from you. But none of your test suites resulted in an error. Everything with the code itself looks excellent. You check the logs. The error is: ```bash Uncaught Error: Cannot read property 'xyz' of undefined at app.min.js:1:45678 ``` You think to yourself, what the heck is `app.min.js:1:45678` supposed to mean? There was no file like that in the entire source code? Your file was called `app.js`. And it's 45678 characters long! That's impossible to debug!! Still, you try to open the file and potentially find the root cause of the error. It's a mess. The entire file is filled with random gibberish you are unable to understand. What should you do? Now, this is where Source Maps come into play. Source Maps allow you to map the minified code in your production environment, _aka the random gibberish you just saw_, with the actual source code, allowing you to pinpoint the root cause of the error in your source code and debug it effectively. In this blog, we will detail what source maps are, why and how they are created, and give some tips on effectively using source maps to debug your code. Let's dive in! ## **Why is source code minified?** Before we delve into Source Maps, let's first decode what happened to your clean, formatted and linked source code and why it looks nothing like it on the browser. The simple answer is minification. Minification is the process of converting your source code into production-ready code without changing any of its functionality. This is typically done by the bundler you are using, such as Webpack. To learn more about bundlers, you can check out this awesome [guide on Javascript bundlers](https://snipcart.com/blog/javascript-module-bundler) by [Snipcart](https://snipcart.com/). Simply put, bundlers optimise your source code by stripping out whitespaces, comments, and redundant code and even removing or renaming variables for shorter alternatives. This makes your code super efficient and much smaller in size. Why does this happen? - **Improved load times** - Smaller file sizes lead to better website loading times. - **Obfuscation—**Although it won't make your code entirely illegible, it does make it harder for regular users to understand. - **Browser Performance** - The code is altered in a way that's easy for browser engines to parse. Here is an example of what a minified React app code looks like: ![Bundle.js file containing minified code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykp4yriiyci828833oct.png) _bundle.js file created by Webpack on building a simple React app, even the text is overflowing from the terminal screenshot!_ ## What are source maps? Source maps are files whose names end with `.map` and that map the minified code to your actual source code. Examples of such files can be `example.min.js.map` or for css, `styles.css.map`. They are explicitly generated only by build tools like Webpack, Vite, Rollup, Parcel etc. Since source maps are only required for debugging purposes, these tools usually have the option to generate source maps off by default. For example to enable it in Webpack, you can do: ```jsx // add this to your package.json file "scripts": { "build:dev": "webpack --mode development --devtool source-map", } ``` or add it to your `webpack.config.js` file: ```jsx module.exports = { devtool: "source-map", // ...rest of your config }; ``` A source map includes crucial information on how the mapping is done, including the actual source file name, the content it includes, various variable name the source code has, name of the minified code file etc. Here is a format of how a typical source map file looks like: ```jsx { "mappings": "AAAA,SAAQA,MAAMA,QAAQ,OAAO;AAC7B,SAAQC...", "sources": ["src/index.js"], "sourcesContent": [ "import React from 'react';\nimport { createRoot } from 'react-dom/..." ], "names": ["React", "createRoot", "App", "count", "setCount", "useState", ...], "version": 3, "file": "bundle.js.map" } ``` The most important section here is `mappings`. This uses a special kind of encoding called [VLQ base 64 encoded string](https://developer.chrome.com/blog/sourcemaps#base64-vlq-and-keeping-the-source-map-small) to map the lines and locations to compiled file and its corresponding original file. ## Visualising source maps "Okay, great!" I hear you saying. "How is this actually helpful? I still can't read the source maps and manually decode the mappings." That's a great question! This brings me to the main highlight of this blog—source map visualisers. These tools allow you to see the mappings in a visual manner to locate and debug the problem effectively. There are many source map visualisers on the market, but today, we will be focusing on Sokra & Paulirish's source map visualization. You can find the source code for this on their [Github Repository](https://github.com/sokra/source-map-visualization/). Here is a side-by-side comparison of how your code (on the right-hand side) can look like a jumbled mess when minified (on the left-hand side). However, the colour-coded mapping of the visualiser helps us map these two codes by hovering over them. ![Comparison between minified code and source code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mpgftukud1078hx2iui4.png) ## Working Example Let’s create a simple React app and play around with it’s sourcemaps! 1. Start with creating a project directory: ```bash mkdir my-project cd my-project ``` 1. Init a new project: ```bash npm init -y ``` 1. Add the following dependencies into `package.json` ```json { "name": "react-counter-app", "version": "1.0.0", "description": "Simple React Counter App", "main": "index.js", "scripts": { "start": "webpack serve --mode development", "build": "webpack --mode production", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@babel/core": "^7.23.0", "@babel/preset-env": "^7.23.0", "@babel/preset-react": "^7.22.15", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", "html-webpack-plugin": "^5.5.3", "style-loader": "^3.3.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" } } ``` 1. Create a `src/index.js` file with following React code: ```jsx // src/index.js function App() { const [count, setCount] = React.useState(0); const increment = () => { setCount(count + 1); }; const decrement = () => { setCount(count - 1); }; return ( Counter: {count} Increment Decrement ); } // New React 18 createRoot API const container = document.getElementById("root"); const root = createRoot(container); root.render(); ``` 1. Add styling by adding a `src/styles.css` file: ```css /* src/styles.css */ .app { font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px; text-align: center; } button { background-color: #4caf50; border: none; color: white; padding: 10px 20px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 10px; cursor: pointer; border-radius: 4px; } button:hover { background-color: #45a049; } ``` 1. Now define the webpack config by creating a `webpack.config.js` file in the root folder: ```jsx // webpack.config.js const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], }, }, }, { test: /\.css$/, use: ["style-loader", "css-loader"], }, ], }, plugins: [ new HtmlWebpackPlugin({ template: "./public/index.html", }), ], devServer: { static: { directory: path.join(__dirname, "public"), }, port: 3000, open: true, }, resolve: { extensions: [".js", ".jsx"], }, }; ``` 1. Now you can start the application by running: ```bash webpack serve --mode development ``` This is how it should look (very basic i know :D): ![Basic UI Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpawfyjl38iit5x40seg.png) You can find the source maps using your browser's dev tools. The format can look different depending on the browser you are using. Here, I am using Zen, but the format should look similar for all browsers. You can do so by right-clicking anywhere on the page and clicking on **Inspect Element**. Then, go to the **Sources** section of your browser and find the source file. Here on Zen, it's available in the debugger section since it's mainly used for debugging purposes. ![Showing source map in dev tools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/55ye3ujrmwuhjw9o62zo.png) Now, you can load this in the [source-map-visualization](https://sokra.github.io/source-map-visualization/). It will look something like this: ![Showing a visualization of source map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/241myprsrlt92gf2a25b.png) On right you can skip all the React code and skip to the section that contains just your code. On hovering over each section of your code you will see exactly which part of the minified code it maps to! It can look pretty confusing at first, but try hovering over various elements in the UI and you will see how intuitive it actually is. For eg. in this example code, ```jsx React.useState(0) ----> t().createElement("h1",null,"Counter: ",n) .... // so on ``` Hovering over `React.useState` reveals that it maps to a `createElement` in the minified code. So our bundler, Webpack, in this case, optimised our code by directly converting our state into a javascript element and directly modifying it in subsequent code. This makes our application much, much more performant and reduces the file sizes the browser has to load! ## Security Considerations While creating the example app, you may have noticed we had to explicitly add the flag `--mode development` to the Webpack run command. This is because source maps are supposed to be used for debugging purposes only, and can lead to security concerns when used in production, including: | Concern | Description | Mitigation | | -------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | | Exposing Source Code | Source maps reveal your original code, including comments and logic | Use `hidden-source-map` or `nosources-source-map` in production | | IP Protection | Intellectual property may be exposed via full source maps | Deploy source maps to secure, authenticated location | | File Size | Source maps can be large, affecting download performance | Generate maps only in development, or serve separately | | Server Configuration | CORS issues may prevent source map loading | Configure proper `Access-Control-Allow-Origin` headers | There are also tools like **Sentry** or **Rollbar** which use your source maps for better error reports without violating any of the security concerns. Tools like these are considered best practise for production environments. ## Conclusion Source maps are a mind-blowing feature that lets you map your source code precisely to the minified code loaded by your browsers, which is generated by bundlers like Webpack for performance and speed. We explored how debugging can be made easy using this feature and tools like source map visualisers to aid in the process. The web is built on top of layers and layers of abstractions done by tools like bundlers, but when things go catastrophically wrong, we might discover that these abstractions are not always hundred percent perfect, and hence, we need to take out our tools, look under the hood, and find the fix ourselves. To learn more about package managers like NPM, Bun, PNPM, and yarn, you can check out my other article, [Mastering npm: A Comprehensive Guide to Package Management](https://www.chiragaggarwal.tech/blog/mastering-npm-a-comprehensive-guide-to-package-management). ## References Thanks to these fantastic references by the Google Chrome dev team that helped me learn about source maps myself and in writing this article: - [Debug your original code instead of deployed with source maps](https://developer.chrome.com/docs/devtools/javascript/source-maps) - [What are source maps? #DevToolsTips](https://www.youtube.com/watch?v=FIYkjjFYvoI) - [Using source maps in DevTools #DevToolsTips](https://www.youtube.com/watch?v=SkUcO4ML5U0&t=241s) --- ## Focus on the product, not the tech stack **Slug:** focus-on-the-product-not-the-tech-stack Recently I got this DM, “Which tech stack should I learn?” “Should I use Next.js or Svelte?” “Is Python better for the backend than node?” I am sure we have asked or been asked questions like this several times in our tech journeys. As someone who is just starting out, I think it seems like a very important thing to ask. You don’t wanna waste your time learning a technology that might become worthless soon? Or isn’t fast enough? or isn’t the “industry standard”… right? Or should it matter? ## Languages are just tools Back in 2003 when Facebook was originally founded, its backend was written in PHP. Was it the lack of options? You can say so. Back then they certainly didn’t have a plethora of frameworks or hell even languages to choose from. But what about now? Realistically PHP should be dead by now… Right? But if you look at the [statistics by Kinsta](https://kinsta.com/php-market-share/), around 79.2% of the internet still relies on PHP to some degree. Okay, but that number has to be declining for sure! But wait... ![PHP Trend Chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iytuwpkuplv3pxtq2ypn.png) Nope, it has been on a steady increase since 2011, peaking at 80.6% in 2015, and maintaining a steady line. But how? It’s because language is just a tool. It does not matter how many new bleeding edge languages enter the market increasing the performance metrics by some fractions of seconds, as long as a ~~tool~~ language sufficiently fulfills its job, it will stay relevant. PHP has done a great job at that. It’s constantly evolving as well, something which is direly required to stay alive in the software world. So next time if someone asks you “Which language should I choose?”, ask them two things: 1. Which language are you more comfortable with? 2. Do the other languages provide a set of features more beneficial for your use case? Even if so are they worth the effort to learn? (arguments like uh it's 0.001sec faster should not be relevant) ## It’s OK to not upgrade Image from: https://foreverealize.me/posts/how-to-update-dependencies/ I was once asked, “Why is your website still at Next version 13 when 15 has arrived?” To which I ask, why does it matter? Sure software is meant to be ever-evolving, but that doesn’t necessarily mean it's a good idea to always hop on the upgrade bandwagon without looking at the consequences. This in this case will be a complete re-write since Next 15 introduces a ton of breaking changes. And it's OK not to upgrade every time. ## Judge a product by its usefulness, not complexity A lot of people especially those building projects just for improving their resume will try to make them extremely complex. It will be a wobbly mess of high-end sounding technologies like SSR, TRPC, GraphQL, Kubernetes, Redis, etc. Sure I do understand the thinking behind this, it does show your ability to work with complex technologies. But in the age of AI and the really fast-moving pace of technology, it's pretty trivial for someone to learn technologies on the go and incorporate them into their project. Real engineering will always be that “**solves a problem, not create one**”. Solving problems always requires asking questions first: 1. Does your project really need TRPC or GraphQL? Why not just use REST like everyone does bruh? 2. Do I really need Kubernetes for those 5 concurrent connections? 3. Does my really interactive website need SSR where then each button click will be a server action and take ages to process any action? ![Tutorial hell meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ikoimzgzcyvgo9c1ot8.png) And please, stop building clones. It does not matter: - how many services you use in it - how much knowledge you gained from creating it - how complex the project was to make The YouTube clone you made by following a random Youtuber’s 8-hour-long guide is not going to help you in any way. Quoting [Theo](https://www.youtube.com/@t3dotgg) (idk where he said tho): > To build a great product, you need to first be a customer If you want project ideas, look around you. Try to solve problems you face every day. If you just try looking, you WILL find many. ## Final words This blog is a summary of my (I admit very short-lived) experience of working in tech till now. I always tried not to treat tech as something I specialize in, but as a tool that helps me solve problems. I started off my journey in web with a simple problem, I just wanted a website of my own because it would be cool :D Then a simple problem gave birth to more problems with a chain reaction ongoing, to me finding myself working with PHP, something I would have never imagined doing in a million years before. So…. that’s it. Keep building and showcasing your work, and most importantly, have fun at it! --- ## Vibe coding an Email Ticket Automater using Postmark **Slug:** vibe-coding-an-email-ticket-automater-using-postmark This is a submission for the [Postmark Challenge: Inbox Innovators](https://dev.to/challenges/postmark). ## What I Built Support tickets… Nobody really loves either writing or much worse reading them. And managing them is much much worse than managing my laundry, and that is a really high bar to pass. So, in a world where we all are writing and summarising emails for the sake of maintaining formality, automating support tickets seems like the logical next step. And sure there are companies that do this… not really going to name them, but my vision here was to just create something that takes the unorganized text and convert it to organized boxes, while still being pretty to look at… _cough_ Jira _cough._ Meet EmailTicket. Firstly hoping that marks are not being deducted for the lack of naming creativity, it's here to do exactly what it says. Create tickets based on emails. Sounds simple right? Well kind of… but with some headaches. Historically, when creating tickets you would have to ask the user a bunch of information like whether it is a billing issue, a bug, or feedback, when did the issue start, etc. After creation, you would have to manually keep track of information like how urgent is it. Has the ticket been closed or is it still in progress? etc. etc. You know… the boring stuff. ![Ticket Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yg0bvs81e953d8r4bu37.png) > Image captured during the pre-AI times (just kidding it was created using ChatGPT) --- But with AI, we no longer need to take care of that stuff. Just give it a random block of text and out comes structured beauty (It’s like a dream come true for data scientists honestly). This is pretty much what Email Ticket does for now. It’s highly inspired by platforms like HelpScout (an amazing tool btw do check it out) and in the future can extend a lot more if good guys like the ones from Postmark support it :D ## Demo Checkout the live link here - [https://email-ticket-automator.vercel.app](https://email-ticket-automator.vercel.app/) If your emails are stuck on processing/waiting, It’s highly likely that I ran out of the 100 free emails. So… don’t sweat too much about it 😅 ## Code Repository ## How I Built It So most of the application is vibe-coded using Lovable. Was it because I am lazy? Probably yes. But that’s not the exact reason. In my opinion tools like Lovable, V0, Firebase Studio (god there are so many now), etc. are perfectly fine to use to get an initial MVP of your idea up and running. And in a world where AI is dominating the entire development process, not utilizing these tools is just slowing you down. So let’s talk about the 10% of coding I actually did. The first was integration with a database. Or more specifically a Backend as a Service because who codes their own backend nowadays? So naturally I went with Appwrite. Lovable refused to do this integration, but thankfully Appwrite is extremely easy to set up. I used it to implement Authentication flow, as well as Database for ticket storing and retrieval. Next was the start of the application, Postmark. Postmark made the entire process of receiving an email, parsing it into JSON, and receiving it just via an API call so easy, I forgot dealing with emails was such an annoying task. Firstly, I needed to A) Either get inbound domain forwarding setup, or B) Get my account approved to be able to send emails to the default inbound email address with no domain restrictions. I tried both techniques and luckily got my account approved in 2 days! ![Postmark Email](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lid9dtnuzzd6gktuziv5.png) Not so luckily I was never able to set my inbound domain even tho I had my MX record set properly and could verify it using https://www.whatsmydns.net/ (You can check my comment here complaining about it - https://dev.to/chiragagg5k/comment/2o6hi). So something to note down @Postmark team. To utilize the API seamlessly without ever needing to touch a single line of actual backend code, I came up with this flow: 1. Appwrite triggers a “create” document/ticket event. 2. The event triggers a function that then calls the Postmark’s API for sending an inbound email. 3. Postmark listens to the inbound email and triggers an email parsing flow. 4. After parsing is done it returns the output to a webhook URL which is just an Appwrite function. 5. The Appwrite function finally processes the parsed content and stores it back to the database. ![Flow Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qsmi3ogtpgd07jqm5k9g.png) Finally time for structured outputs. Almost all LLMs offer a way to generate structured outputs now in their APIs. Here are some docs I found for this: 1. https://docs.perplexity.ai/guides/structured-outputs 2. https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses 3. https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency and on and on… (a small plug would be to check this repo I worked on which allows you to use any LLM with structured outputs and consistent syntax in PHP - https://github.com/utopia-php/agents) You can check the [repository code](https://github.com/ChiragAgg5k/email-ticket-automator/blob/main/functions/email-parsed-webhook/src/main.js) itself to see which one I picked because I might have changed it by the time you are reading it :p That’s it then. Let me know in the comments how was your experience using EmailTicket Automater. You can reach out to me on any of my social media links if you have any queries or just wanna have a casual talk. A big shoutout to Postmark for arranging this contest and letting me have fun with their platform, as well as my company Appwrite for supporting the application’s backend :D Bye! --- ## Automate anything - Making research analysis effortless **Slug:** automate-anything-making-research-analysis-effortless Starting with a research project can be overwhelming. There are so many data points to consider, so many hours of reading to do, and within all that chaos, the hardest piece of the puzzle to find is a **starting point**. I faced this first hand when last year I had to work on multiple papers to earn a single credit point for my engineering degree. Unfortunately why universities force this pointless system is not the spotlight of today’s discussion. Instead, we are going to discuss how you can cheat on this mundane activity using Runner H. The article will redefine how you interact with AI, helping you put together analysis, execution, summarisation and integration with a single prompt. So, let's dive in. ## Researcher's Dilemma Whenever you start with a research project, the first question that comes to mind is: **"Has this been researched before?"** Answering this question is crucial for: 1. Avoiding duplication of work 2. Finding gaps in the existing study 3. Not claiming something as noble when it has already been done before However, doing this is a very manual process, and requires hours and hours of find papers, reading them and doing accurate analysis. ## Meet Runner H Ever tried using Perplexity or ChatGPT in research mode? Think of Runner H as that, but on **steroids**. Runner H is an advanced AI-powered web automation tool that lets you perform multi-step online tasks using natural language instructions. It seamlessly integrates with services like Slack, Notion, Gmail, and Excel, enabling you to create complex workflows, no coding experience required. For our purpose, we can use Runner H to: 1. Quickly identify similar research papers with a Google Scholar scan. 2. Assess similarities between them and your topic using AI. 3. Scan the internet to find gaps between the existing work. 4. Export the findings in a structured format. ## The prompt Below, I have crafted a prompt that you can copy and paste into Runner H, replace with the topic you need to research, and voila~, see the magic unfold: _Analyse if my research idea, `{YOUR_RESEARCH_IDEA}`, has been previously explored. Search Google Scholar for similar research papers. For each, extract the title, authors, publication date, abstract, journal/conference, and similarity percentage to my idea, and note any research gaps mentioned or left open. Create an Excel file summarising all findings._ Just replace `{YOUR_RESEARCH_IDEA}` with the idea you want to research on. ## Demo Let's try researching on a topic: **"The impact of generative AI tools on student learning outcomes in higher education"** So Runner H after doing all the analysis and hard work for us was able to generate a structured output. Unfortunately, even though prompt specifically asks for Excel format, most of the times for me it generated a PDF format for me. Still Runner H is still in early phases and hopefully will be improved in future by the team. ## So what makes Runner H different? One of the most fascinating differentiations I found in Runner H compared to other agentic tools was its ability to search, summarise and synthesize as well. Instead of just dumping in raw results, it evaluates the relevance of each paper, highlights the overlaps and gaps in the study, and presents it in an actionable format. Using Runner H has completely transformed my approach to starting a research topic. It has bridged me to the starting point where I used to struggle the most. For students, academic professionals, and anyone doing any sort of research work, Runner H is a game changer. --- _Happy researching! If you have questions or want to share your own experiences with Runner H, drop a comment below._ --- ## From student to full-time Platform Engineer at Appwrite **Slug:** from-student-to-full-time-platform-engineer-at-appwrite ## TLDR For those who are not really interested in reading some random university student’s story, here is a short TLDR. I worked at Appwrite as an Engineering Intern for 6 months and just got offered a full-time position as a Platform Engineer, while still being in University for another year :D The story ahead covers how while drowning in a sea of desperation a light of hope changed everything for me, how my experience was working for a fully remote OSS company, and the challenges I faced while working in PHP, a language I had never touched before this :p --- ## Pre-Interview Back in September-November 2024, I was kind of at the lowest point of my engineering journey yet. I had concluded my last internship around August when the project I was working on abruptly closed, and since then I was on a constant hunt for my next one. Silence. From all the mediums I tried - LinkedIn, Upwork, AngelList, cold DMs, and what not. The thing is that once you start “working”, although it was an unpaid one, it’s pretty hard to go back to doing… nothing. “Nothing” is the wrong word I guess, I was still working on OSS, building projects, and trying to learn new things. But if someone asked what I was up to nowadays… i drew a blank. Then one day I was just hanging out on Discord and saw a notification from a bright red server I usually didn’t check much. Hey, it’s another job opening, so I applied like usual. Another month passed, silence again. But then I saw an email from Emma, our People’s person at Appwrite. “We wanna have a chat with you.” This was a very unusual email for me. Usually, companies like to give an assignment for the first round, but here it was a direct meeting scheduled… so I already knew something was different this time. ## The Interview(s) Of course, I am not allowed to share the entire interview experience without getting into a call with Emma after this is published. So, I just want to share one question that has stuck with me since then, “Looking at your resume, you don’t seem to have much if any experience with PHP. And Appwrite’s entire backend is in PHP. Will you be able to handle this?” - Torsten, Product Engineer at Appwrite. Stunned. Before going to the interview I did see the codebase was written in PHP. I had never touched it before. No contributions, No issues filed. So I assumed the position I was applying for might have nothing related to this codebase… maybe a new project altogether… right? Nope. My answer? It was somewhere along the lines of “I will figure it out, trust me, bro”. Very bad answer I know. But it was the best I could come up with. After the interview, I went on a grind. Reading the codebase, running it, and understanding its architecture (in that process I even wrote an article - [Architecture Patterns for Beginners: MVC, MVP, and MVVM](https://dev.to/chiragagg5k/architecture-patterns-for-beginners-mvc-mvp-and-mvvm-2pe7)). Long story short I tried to learn as much about Appwrite’s codebase as was physically possible in that timeframe from the next interview scheduled. And Tada~ It worked. I got the offer! ## Experience Working at Appwrite is unlike working in any other place. From the first day on, even though you are an Intern, you are never treated like one. I was introduced to the entire codebase in my first week and assigned to issues not from the pending log, but actual production issues that users were facing. You can message anyone. At any point. Work on any issue that you like. There is a padding issue you saw on the website? Knock yourself out and raise the PR. The beautiful thing about Appwrite being open source is that you could do this from the very start. It’s just now I could bug anyone on the team more freely if I was stuck on any issue. It’s hard to summarise how much work I did in the past 6 months, but here are some notable ones: 1. 200+ PRs merged in the Appwrite organization’s repositories, including main, website, SDK, MCP, etc. repositories. 2. Uptime monitoring for the [Appwrite Assistant](https://appwrite.io/docs/tooling/assistant). 3. Adding a monthly limit to the number of free Phone OTPs allowed. 4. Fixing Amazon and Slack OAuth adapters. Add Figma OAuth adapter. 5. Making several changes to SDKs and making releases. 6. Adding Types generation to Appwrite CLI. 7. Worked on the DevKeys feature in the latest Appwrite release 1.7 8. Added 2 blogs on the website - self-hosting using coolify and announcement for [DevKeys](https://appwrite.io/blog/author/chirag-aggarwal). 9. Taking leadership on the [Synapse](https://github.com/appwrite/synapse) project, operating system gateway for remote serverless environments. 10. Adding new phone OTP adapter - Inforu, which reduces pricing for phone OTPs for Israeli users by 11x. 11. and… a lot more. Honestly looking back at it, I never realized I worked on so much. It feels like I had just joined Appwrite yesterday. ## Thank You At last, I wanna thank the entire Appwrite team for treating me like part of the team since day one, and for giving me this opportunity. Let’s build like a team of hundreds\_ --- _You can check out my profile to learn more about me - https://www.chiragaggarwal.tech/_ _Hit me up on any socials if you have any doubts, I am always up for a chat :D_ --- ## JStack + Appwrite: A Match Made in Heaven for Modern Web Development **Slug:** jstack-appwrite-a-match-made-in-heaven-for-modern-web-development If I had a penny for each time a youtuber has launched his own tech stack, I would have 2 pennies, which isn't much but its weird that it happened twice. I am talking about [T3 Stack](https://create.t3.gg/) launched a while back by everyone's favourite Theo, but recently a new player has entered the market called [JStack](https://jstack.app/), by Josh who is the lead Devrel at Upstash. To be fair, its not even that recent, but as always ~~I am late to the party~~ I try to give a framework time to mature and gather feedback from community before giving it a shot. So, did I prefer JStack over T3 stack? Did it have more compatibility with Appwrite, my favourite backend provider? Can it be hosted on Appwrite Sites? Let's find out. ## Getting Started Let's start with initialising the project: ```bash bunx create-jstack-app@latest ``` Options selected: ```bash ┌ jStack CLI │ ◇ What will your project be called? │ testing-jstack-appwrite │ ◇ Which database ORM would you like to use? │ None │ ◇ Should we run 'bun install' for you? │ Yes Using: bun ✔ testing-jstack-appwrite scaffolded successfully! ``` Let's just quickly run a dev server to see what we get out of the box: ```bash cd cd testing-jstack-appwrite bun dev ``` ![JStack landing page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z9c9q9ae61yzugugap9b.png) ## Initialising Project On skipping the ORM option, the stack still sets up a `/src/server` folder with an example posts router. But it only mocks the DB using an array which does not persist: ```tsx // Mocked DB interface Post { id: number; name: string; } const posts: Post[] = [ { id: 1, name: "Hello World", }, ]; ``` We skipped the ORM option because Appwrite provides built-in schema management through its SDK, eliminating the need for a separate ORM layer. To get started, let's head over to [https://cloud.appwrite.io](https://cloud.appwrite.io/) to set up our project. If you are using Appwrite for the first time, I highly recommend you check out our [Start with Web docs](https://appwrite.io/docs/quick-starts/web). Here is a quick setup guide: - Create a new project - ![New project model (Appwrite)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6w0nn7x3pt9x8tott78.png) - Add a new web platform and select Next.js - ![New platform screen (Appwrite)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w2cvx6dkh8zdrntpy4js.png) - Go to project overview and grab your project's ID and region-specific endpoint: ![Project ID and endpoint (Appwrite)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jom1n3wjbri8qpko7z7a.png) - Create a new `.env` file in your project directory and paste in those values: ```env NEXT_PUBLIC_APPWRITE_PROJECT_ID=686a271700323696d223 NEXT_PUBLIC_APPWRITE_ENDPOINT=https://fra.cloud.appwrite.io/v1 NEXT_PUBLIC_APP_DOMAIN=localhost # we will change it later ``` - Now, let's initialise the Appwrite SDK in the project using: ```jsx bun add appwrite ``` This covers up on how to initialize Appwrite in normal Next.js project. Now we need to configure our Appwrite databases according to the project. For this demo, let's create a `Posts` collection to shift the mock database JStack uses as an example to Appwrite. ## Defining Schema - Go to the Appwrite Console > Databases > Create Database. We will call it `main` , and keep it' ID as main as well: ![Create database (Appwrite)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rsmpij8n1yqok80pfm15.png) - Similarly, create a collection `posts` and keep its ID as `posts`. - Each document in Appwrite already has an unique ID attached to it, so the only attribute we need is `name` for now: ![Create string attribute (Appwrite)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/caheyx67pondngmv139t.png) - Lastly, you will need to define who can access your collection. To learn more about it, check out the docs for [Appwrite Permissions](https://appwrite.io/docs/advanced/platform/permissions). For now, we will set it to any: ![Create permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2m4jl1939fxqa93olacw.png) ## Syncing Types That's it, we are all set with configuration on the Appwrite Console. Now let's set it up in our project. I will also take the help of the Appwrite CLI to help me set up things faster. You can learn more about how to install it by following the installation docs. Once done, run: ```jsx testing-jstack-appwrite appwrite init project ? How would you like to start? Link directory to an existing project ? Choose your organization 67610b8ee51f147ca943 ? Choose your Appwrite project. [object Object] ✓ Success: Project successfully linked. Details are now stored in appwrite.json file. Would you like to pull all resources from project you just linked? Yes ``` Once done, let's utilize the last Types Generation feature to sync our defined types: ```jsx appwrite types src/types ℹ Info: Detected language: ts ℹ Info: Directory: src/types does not exist, creating... ℹ Info: Found 1 collections: posts ℹ Info: Found 1 attributes across all collections ℹ Info: Added types to src/types/appwrite.d.ts ✓ Success: Generated types for all the listed collections ``` Result will look something like this: ```jsx /** * This file is auto-generated by the Appwrite CLI. * You can regenerate it by running `appwrite types -l ts src/types`. */ export type Posts = Models.Document & { name: string; } ``` Obviously pretty small for now, but a really helpful feature once the project expands and more collections are defined. ## Configuring Code Final steps are to connect the Appwrite backend with our tech stack. For that let's create a simple `appwrite.ts` file in `src/lib` folder: ```jsx const client = new Client() .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!) .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!); const databases = new Databases(client); export { client, databases }; ``` Now, we can modify the original `post-router.ts` file to use the defined database: ```jsx const DATABASE_ID = "main"; const POSTS_COLLECTION_ID = "posts"; export const postRouter = j.router({ recent: publicProcedure.query(async ({ c }) => { const posts = await databases.listDocuments( DATABASE_ID, POSTS_COLLECTION_ID, ); return c.superjson(posts.documents.at(-1) ?? null); }), create: publicProcedure .input(z.object({ name: z.string().min(1) })) .mutation(async ({ c, input }) => { const post = await databases.createDocument( DATABASE_ID, POSTS_COLLECTION_ID, ID.unique(), { name: input.name, }, ); return c.superjson(post); }), }); ``` And done! Now your JStack application is using Appwrite as its backend provider. ## Testing - Start the dev server if not already by running: ```jsx bun run dev ``` - Your application should have started on: http://localhost:3000/ - Create a new post. - You should be able to see it in recent posts: ![Recent posts image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oy9htyvp77qqct8grhzf.png) - Also, the data should be visible in your Appwrite Console: ![Appwrite console showing data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oce30jvc16lb80e7v9qu.png) ## Deploying your application Until recently, to deploy your Next.js application, you had pretty low choices. But no more [Appwrite Sites](https://appwrite.io/products/sites). Now, you can have your backend and frontend hosted on Appwrite. Check out these docs on how to get started with Sites - https://appwrite.io/docs/advanced/self-hosting/sites Let's deploy your application using Sites: - Go to Appwrite Console > Sites > Create Site. - You can upload a tar file directly, or connect to a GitHub repository (I prefer the GitHub option for automatic deployments). - Select the repository: ![Selecting repository screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6ww9m9mb2z7w8hc5ggk.png) - You can keep all the settings as the default ones, just make sure to upload the environment variables we defined earlier: ![Add environment variables screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wsthkangby5vgqfnntio.png) - Depending on the domain you are assigned / plan to use, you also need to update the `APP_DOMAIN` variable we defined earlier. For me I will keep it: ```jsx NEXT_PUBLIC_APP_DOMAIN = jstack - appwrite - template.appwrite.network; ``` - Click on deploy. And done! Your application should be live 🎉 ![Deployment successful screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfxrz2dfa7nhwu26n93o.png) You can checkout the demo application here - https://jstack-appwrite-template.appwrite.network/ ## Conclusion Let's start by answering the question the blog began with: Do I prefer JStack over T3 Stack? I'm sorry, Theo, but I do. JStack solves the fundamental problems I have always had with Next.js: - Using [Hono](https://hono.dev/) instead of Next.js's inbuilt convention for defining API routes. - Using [TanStack](https://tanstack.com/) query out of the box. Trust me, you should start now if you are not using it. - Type safe and uses [Zod](https://zod.dev/). The most noticeable difference between T3 Stack and JStack is how light they are, mainly due to the inclusion of the TRPC protocol in T3 Stack. Most projects do not require it, and it makes the code 10x more complicated to maintain, in my opinion. So, give JStack a shot if you are starting with a new project (or want to spend a weekend migrating your existing stack to it). Huge shoutout to [Josh](https://www.youtube.com/@joshtriedcoding) for creating this wonderful stack. And lastly, give Appwrite a chance to be your next all in one cloud platform, both for your backend and frontend needs. ## Sources: - JStack - https://jstack.app/ - Appwrite Docs - https://appwrite.io/docs - Github repository - https://github.com/ChiragAgg5k/jstack-appwrite-template --- ## How we solved logging at Appwrite **Slug:** how-we-solved-logging-at-appwrite A few weeks back I came across a post by [@boristane](https://twitter.com/boristane) sharing a website he made, [loggingsucks.com](https://loggingsucks.com). It caught my eye because it had been shared by my favorite tech YouTuber, [@theo](https://twitter.com/theo). Like most people, I was really inspired by the article and shared it with my team. [@lukebsilver](https://twitter.com/lukebsilver), Appwrite's Engineering Lead, was also inspired by it and decided to work on a new PHP library, `utopia-php/span`, to fix logging throughout the Appwrite codebase. ## What we had Before this, Appwrite used a combination of two different libraries targeting logging in two different areas: - `utopia-php/console` — a very simple wrapper library around stdout logging using functions like `Console::success()`, `Console::error()`, etc. - `utopia-php/logger` — an adapter-based library to push error logs to monitoring systems like Sentry, AppSignal, Raygun, etc. Combined, these libraries served their purpose for a long time, but we often ran into problems when debugging production issues, the same ones the original article discusses in detail. I'd highly recommend going through [that article](https://loggingsucks.com) first so I don't repeat it all here. ## Our solution Funnily enough, the first tricky problem was deciding on a name. "Logger" was already taken, so we had to be creative. The word "Span" captured exactly what we were trying to solve: a fundamental unit of work with a named, timed operation alongside various attributes, errors, trace IDs, etc. The first step was to move away from simple log lines to **structured logging**. Span enforces this by only exposing a single primary method, `add()`, which accepts a key-value pair. Before: ```php Console::info("Deleting project {$project->getId()} (type={$type}, region={$project->getAttribute('region')})"); ``` After: ```php Span::add('project.id', $project->getId()); Span::add('project.type', $type); Span::add('project.region', $project->getAttribute('region')); ``` This massively improved the queryability of our logs — one of the things we struggled with most when going through logs in production. We also wanted the library to be extremely simple to use. Earlier, with "logger", we had to hop through various dependency injection loops just to use it: ```php public function action( Message $message, Document $project, Log $log, // ← has to be injected just to add a tag ): void { $log->addTag('projectId', $project->getId()); $log->addTag('type', $payload['type']); // ...actual work... } ``` With Span, it's much simpler: ```php public function action( Message $message, Document $project, ): void { Span::add('projectId', $project->getId()); Span::add('type', $payload['type']); // ...actual work... } ``` ## Why not just make the logger methods static? Because Appwrite's codebase leverages coroutines (via Swoole) for concurrency between requests, similar to goroutines in Go. A naive static implementation would leak state across concurrent requests. Span solves this by allowing you to choose the storage type: ```php Span::setStorage(new Storage\Coroutine()); ``` ## Exporters To combine both logger and console capabilities, Span exposes built-in Exporters, which, as the name suggests, export the logs to not just stdout but any supported adapter. The library currently supports three: 1. **Stdout**: basic usage. Dumps the output as plain JSON: ```json { "action": "worker.deletes", "span.trace_id": "7a3f9c2b4e1d8f06", "span.duration": 1.92, "project.id": "67f3a9", "project.type": "projects", "project.region": "fra" } ``` 2. **Pretty**: JSON dumps are very useful in production where you have OpenTelemetry or other monitoring set up, but locally you just want things to be readable: ```plaintext worker.deletes · 1.92s · 7a3f9c2b project.id 67f3a9 project.type projects project.region fra ──────────────────────────────── ``` 3. **Sentry**: since Sentry is primarily an error tracking system, Span also exposes a callable "sampler" that lets you filter which logs get exported to a particular exporter: ```php Span::addExporter( new Sentry(dsn: '...'), // Sampler: drop noisy expected errors, keep everything else. sampler: function (Span $span): bool { $error = $span->getError(); return !($error instanceof ExecutorException) || $error->isPublishable(); }, ); ``` ## Before and after One massive improvement we saw was with error logs. Before, we had very verbose and noisy errors that were often hard to make sense of: ```plaintext [Error] Timestamp: 2026-04-17T10:32:16+00:00 [Error] Type: Utopia\Database\Exception\Timeout [Error] Message: Query took too long [Error] File: /usr/src/code/src/Appwrite/Cloud/Platform/Workers/Deletes.php [Error] Line: 214 Trace: #0 /usr/src/code/app/worker.php(828): ... ``` Now: ```json { "action": "worker.deletes", "span.trace_id": "7a3f9c2b4e1d8f06", "span.duration": 2.14, "project.id": "67f3a9", "error.type": "Utopia\\Database\\Exception\\Timeout", "error.message": "Query took too long", "error.file": "/usr/src/code/src/Appwrite/Cloud/Platform/Workers/Deletes.php", "error.line": 214, "error.trace": [ { "file": "/usr/src/code/app/worker.php", "line": 828, "function": "action" } ] } ``` If you're writing PHP in 2026, give [utopia-php/span](https://github.com/utopia-php/span) a shot. And a massive shoutout to [@lukebsilver](https://twitter.com/lukebsilver), who actually built the library. I just learned from him and wanted to share what I picked up. --- ## Managing multiple Docker Hub accounts using docker-use **Slug:** managing-multiple-docker-hub-accounts-using-docker-use Most of the time I'm signed into my work Docker Hub account, and that's fine. Almost everything I build ends up there. Then one weekend I was playing around with [openruntimes/orchestrator](https://github.com/open-runtimes/orchestrator), an orchestration service for running containerized workloads with callbacks. I had a PR open with some small tweaks, and I wanted to publish release-candidate images so I could pull them down and actually test end-to-end. I didn't want those RC images sitting in my work namespace. So for the first time, I actually needed to switch Docker Hub accounts on my machine, and there isn't a good way to do it. The official answer is `docker logout`, then `docker login` again. Docker does respect a `DOCKER_CONFIG` environment variable that points at a config directory, but you still have to set it yourself every time. A few years ago I'd have spent the afternoon hunting for someone else's tool. In the age of AI, you can just build the one you wished existed. So I did. The result is [`docker-use`](https://github.com/chiragagg5k/docker-use). ## The idea The fix is small. Docker already respects the `DOCKER_CONFIG` environment variable, and whatever directory it points at is treated as the active config. So the whole mechanism is: 1. Give every account its own config directory. 2. Let the shell flip `DOCKER_CONFIG` between them by name. No patches to Docker, no daemon involvement, no new credential store. A directory per account, plus a shell function to switch between them. ## What it looks like ```sh # one-time per account docker-use add appwrite -u my-appwrite-username docker-use add personal -u chiragagg5k # day-to-day docker-use appwrite docker-use personal docker push chiragagg5k/job-sidecar:temp-appwrite-pr-4f081ed-20260521-r2 docker-use whoami # which one am I on right now? docker-use list # all accounts ``` `add` shells out to a normal `docker login`, so 2FA and access tokens behave the way you already expect. ## How it works There are three pieces. Each account lives in its own config directory, at `~/.docker-accounts//config.json`. Names are validated against `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$` so they can't escape the directory or surprise the shell. The trickier piece is a shell wrapper that mutates the parent shell. A child process can't change its parent's environment variables, so the binary on its own can't make `DOCKER_CONFIG` stick in your shell. The workaround is a shell function, generated by `docker-use init zsh`, that you source in your rc file: ```sh eval "$(docker-use init zsh)" ``` The wrapper calls the binary, reads the printed config path, and assigns it to `DOCKER_CONFIG` in the current shell. It's a plain assignment, not an eval on the path itself, so a weird config name can't smuggle anything into your shell. `zsh`, `bash`, and `fish` all work. The last piece is keeping Docker's credential helpers in place. When `docker login` finishes, the resulting `config.json` often contains `"credsStore": "osxkeychain"` (or `secretservice` on Linux). If you wipe that field, Docker Desktop's keychain integration breaks and the next push prompts you for a password. So during `add`, `docker-use` reads the config Docker just wrote and preserves the credential helper settings. ## Things I'd flag if you read the source A few decisions that weren't obvious going in. The shell wrapper assigns the config path directly instead of eval-ing whatever the binary prints. Eval-ing user-controllable output tends to age badly. Account names get validated before any filesystem operation runs. `docker-use add ../etc -u …` is not a debugging session I want to have. `remove` asks for confirmation before deleting. It's a destructive action on credentials, so the extra keystroke is worth it. ## Install ```sh brew tap chiragagg5k/tools brew install docker-use ``` Then drop this into your `~/.zshrc` (or `~/.bashrc`, or `~/.config/fish/config.fish`): ```sh eval "$(docker-use init zsh)" ``` Binaries for other platforms are on the [releases page](https://github.com/chiragagg5k/docker-use/releases). It's a small tool for a small problem, but the kind of friction you live with for years before noticing you don't have to. If you end up using it, I'd love to hear what works and what doesn't. Open an issue or reach out directly. --- ## How I built the Appwrite MCP server (and decided to hide most of its capabilities) **Slug:** how-i-built-the-appwrite-mcp-server When Anthropic introduced the Model Context Protocol on November 25, 2024, it got everyone's eyes on it, including Christy, who was Appwrite's Engineering Lead back then. I had just started my role as an "Engineering Intern" and had no idea what a whole new protocol meant, or why it was such a big deal. Looking at the surface, I wasn't entirely wrong. MCP is JSON-RPC with a schema and a handshake stapled on. What took us sixteen months was everything stapled around it. MCP launches"] --> B["Feb 2025stdio ships"] B --> C["Mar 2025StreamableHTTP"] C --> D["Apr 2026981 → 4tools"] D --> E["Jun 2026hostedmerges"] E --> F["Jul 2026shipped"] `} caption="Streamable HTTP did not exist when MCP launched. It replaced HTTP+SSE in the 2025-03-26 revision." /> ## The stdio years Christy had a working stdio server in the repo by February 26, 2025. We already had API keys, so the wiring was simple: ```bash claude mcp add appwrite \ --env APPWRITE_PROJECT_ID= \ --env APPWRITE_API_KEY= \ --env APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 \ -- uvx mcp-server-appwrite ``` An API key is scoped to exactly one project by design, so the ceiling was baked into the credential. Switching projects meant editing your editor config. Creating a project was impossible. So was anything at the organization level. The credential is the whole difference between the two transports, and everything hard about the hosted version follows from swapping it for a token that belongs to the user instead of the project. ## Authorization ate the schedule By the spec, authorization is genuinely optional: > Authorization is OPTIONAL for MCP implementations. [...] Implementations using an HTTP-based transport SHOULD conform to this specification. For a service where one tool call can drop a database, we weren't comfortable treating it as optional. If you use Auth0 or WorkOS, this is a config screen. Appwrite keeps everything in-house, so Matej built the authorization server itself, and I built the resource server plus whatever Cloud was still missing before real clients would work. >M: POST / (no token) M-->>C: 401 + WWW-Authenticate C->>M: GET protected-resource metadata M-->>C: authorization_servers (9728) C->>A: POST /register (7591) A-->>C: client_id C->>A: /authorize + PKCE + resource (8707) A-->>C: consent, then code C->>A: /token + code_verifier A-->>C: token, aud = mcp.appwrite.io C->>M: POST / + Bearer M->>M: verify JWKS, issuer, aud M-->>C: tool result `} caption="Steps 2 through 6 are the part that makes 'just paste this URL' work. Nothing is pre-provisioned." /> Three RFCs carry that flow. Protected Resource Metadata (RFC 9728) is the only real MUST in the whole authorization spec: ```json { "resource": "https://mcp.appwrite.io/", "authorization_servers": ["https://cloud.appwrite.io/v1/oauth2/console"], "scopes_supported": ["..."], "bearer_methods_supported": ["header"] } ``` Resource Indicators (RFC 8707) put our canonical URI into the token's `aud`, so a token minted for another service can't be replayed against us. Dynamic Client Registration (RFC 7591) is what lets a client self-register. Add PKCE with `S256`, RFC 8414 discovery, and you have the shape of it. The RFCs are documented. What isn't documented is that every client reads them differently, and you find out in production: - **Raycast** was on the `2025-03-26` authorization spec, which looks for `/.well-known/oauth-authorization-server` instead of the protected-resource route. I only found it by putting a logging proxy in front of the server and watching what it actually asked for. - **Claude Code** re-authenticated every single run. It listens on an ephemeral loopback port, so the redirect URI never matched. OAuth's native-app BCP (RFC 8252 §7.3) says you must allow any port on `127.0.0.1`. We weren't. - **Our own scope catalog** broke the flow. All ~118 granular scopes produced a `scope` parameter of ~2,680 characters against a validator capped at 2,048. Nobody ever reached a consent screen. One warning if you're about to build this: RFC 7591 went from SHOULD to MAY in `2025-11-25` and is deprecated as of `2026-07-28`, replaced by Client ID Metadata Documents. We shipped that too. This part of the spec is still moving. ## Sessions, and then no sessions The `2025-06-18` spec let a server hand out an `Mcp-Session-Id` alongside the `InitializeResult`, with `DELETE` to terminate and `Last-Event-ID` for resumability. We skipped all of it: ```python StreamableHTTPSessionManager(app=server, json_response=False, stateless=True) ``` Every request carries a bearer token. Verify it, build a client from it, serve the call. Nothing to store, nothing to lose on restart, nothing to make sticky across replicas. That turned out to be the right bet for a reason I can take no credit for. The `2026-07-28` revision removed sessions from the protocol entirely. `Mcp-Session-Id`, the `initialize` handshake, the GET SSE stream: all gone. What we do carry is version negotiation, because you don't get to pick your clients' protocol version. ## Off-topic, but why not REST? Many intellectuals like myself must have wondered why MCP exists at all. Can't this be 100x simpler with, I don't know, REST? The model only knows its training data plus whatever you hand it at runtime. If it has never seen Appwrite, it will never guess this: ```http POST https://.cloud.appwrite.io/v1/tablesdb X-Appwrite-Project: X-Appwrite-Key: Content-Type: application/json { "databaseId": "unique()", "name": "Production" } ``` The endpoint, the header names, the fact that `unique()` is a magic value. With MCP the same operation shows up self-describing: ```json { "name": "tables_db_create", "description": "Create a database in an Appwrite project", "inputSchema": { "type": "object", "properties": { "databaseId": { "type": "string" }, "name": { "type": "string" } }, "required": ["databaseId", "name"] } } ``` You can be happy knowing AI needs a lot more handholding than you do (for now). ## Tool choice Every MCP server I looked at ships a small, curated set. Appwrite generates one tool per SDK method, which lands at 981 methods across 38 services. There's no version of "expose them all" that works, for two unrelated reasons. **The clients won't take them.** In early 2025 Cursor documented that it "will only send the first 40 tools to the Agent" and truncated silently. Windsurf refused outright above 50. That was with per-service flags already in place, which is the part that stings. A community user opened [issue #17](https://github.com/appwrite/mcp/issues/17), "Please reduce the number of tools": > Cursor has 40 MCP tools limit to use, but Appwrite solely has 195 tools, so it cannot be used with other tools nor even all of Appwrite tools. I pointed out you could narrow it with `--databases`. The reply: > That's quite non-sense. Then do I have to edit MCP parameter settings for each time whenever I do another jobs...? And anyway, `--databases` solely has 42 tools, which already exceeds Cursor's recommended limit (40). **Quality falls off well below the caps.** The numbers converge from unrelated directions. Anthropic puts degradation at "once you exceed 30-50 available tools". OpenAI says "fewer than 20 functions at the start of a turn". Block's Goose recommends 50 or fewer. And the fix measures well: Anthropic's [advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use) work takes Opus 4 from 49% to 74% on MCP tool-use evals with a search tool enabled, and Opus 4.5 from 79.5% to 88.1%, with 85% fewer tokens on definitions. [RAG-MCP](https://arxiv.org/abs/2505.03275) more than triples selection accuracy (43.13% against 13.62%). The caveat, because it cuts against me: [MCPVerse](https://arxiv.org/abs/2508.16260) found some agentic models handle big action spaces fine. Claude-4-Sonnet scored 62.3 with an oracle tool set and 62.4 with ~220 tools. Big catalogs aren't fatal. They're a tax you're paying for nothing when the agent needs three tools out of 981. ## Four tools over 981 - `appwrite_get_context` answers where you are and which projects you can see - `appwrite_search_tools` searches the hidden catalog in natural language - `appwrite_call_tool` calls one of them by name - `appwrite_search_docs` searches the Appwrite docs semantically Search narrows at request time, which is why the per-service flags could be deleted entirely. Mutations require `confirm_write: true`, and results too large for the conversation become MCP resources instead. The scoring behind `appwrite_search_tools` is deliberately dumb: token and substring matching against the tool name, description, service and resource, a bonus when the query's inferred verb matches the tool's, a penalty when it doesn't. No embeddings, no index to rebuild, no inference call in the hot path. Here's what a client sees: What made me stop second-guessing the design is that we weren't alone. Stripe put its whole API behind `stripe_api_search`. Sentry exposes 9 of 46 through `search_sentry_tools`. GitHub removed its dynamic toolset tools and looks to be building a search replacement. Three companies with no reason to coordinate landed on search-then-execute in the same window. ## Where it landed ```bash claude mcp add --transport http appwrite https://mcp.appwrite.io/ ``` No API key, no project ID, no config editing to switch projects. Projects and organizations are parameters on the call now instead of properties of the credential. stdio didn't go away. I removed it in the hosted refactor and put it back two days later, because self-hosted users need it. It runs on a project API key and gets 647 of the 981 methods, since a project key can't reach console-level operations anyway. Behind that URL there's also OpenTelemetry, Sentry, Grafana dashboards, and region routing so a project in another Cloud region doesn't return `general_access_forbidden`. You end up operating a service, not publishing a package. That's the part I underestimated most. ## What I'd tell anyone building one The transport is not where the time goes. The authorization spec and everything it pulls in is where the months disappear. Test against real clients early and expect them to disagree. A logging proxy in front of your server was worth more to me than another pass through the docs. Assume the spec moves under you. Between starting and shipping, sessions were removed, RFC 7591 was deprecated, and a stateless revision landed. Anthropic donated MCP to the Agentic AI Foundation in December 2025, so it isn't even one vendor's project anymore. And don't hand your API surface over as your tool surface. The architecture this server has today came out of a bug report from a user who was annoyed with us, which I think is the correct way for this to have gone. The server is open source at [github.com/appwrite/mcp](https://github.com/appwrite/mcp), and the hosted one is at `https://mcp.appwrite.io/`. --- ## How utopia-php/client keeps fixing our memory leaks **Slug:** how-utopia-php-client-keeps-fixing-our-memory-leaks One of our billing workers kept dying. It would sit flat for hours, then climb into its memory limit in about four minutes and get killed. Kubernetes restarted it, and later the same day it happened again. Six times a day, every day. The obvious first move is to look at what PHP is holding on to, and that's where it got strange. When PHP runs out of memory it tells you: a fatal error, a stack trace, the allocation that tipped it over. We had none of that. `memory_get_usage()` reported a flat 6 MB the entire time, right up to the moment the process vanished. Those two facts together are the whole puzzle. Something was eating memory, and it wasn't the PHP heap. It was cURL, two sockets at a time, and the cause turned out to be a missing keyword. We've chased this same shape in more than one Appwrite service now, and the fix has been the same every time: stop building a new HTTP client for every call, and move the library onto `utopia-php/client`. ## What was actually leaking The worker idles at ~330 MB against a 512Mi limit, which is the flat stretch below. Once a day the newly-due invoice batch runs, about a thousand invoices with several Stripe calls each, and it goes from comfortable to dead in roughly four minutes. Luke traced it, and the code turned out to be completely unremarkable. `Pay\Adapter::call()` built a `new Utopia\Fetch\Client` for every request. In a one-shot FPM request that's fine; the process exits and the OS reclaims everything. In a long-lived worker it's a slow bleed, and the reason is a reference cycle that has nothing to do with the PHP heap. `curl_setopt` stores your write callback on the `CurlHandle`. A closure declared inside an instance method captures `$this`. So the handle holds the closure, the closure holds the adapter, and the adapter holds the handle. Nothing can be freed by refcounting, and `__destruct`, with its `curl_close`, only runs when PHP's cycle collector fires. By default that means after 10,000 cycle roots have piled up. Until then every request strands an open keep-alive connection. Reduced down, that's this: ```php final class Curl { private ?CurlHandle $handle = null; // the adapter holds the handle public function send(Request $request): Response { $body = ''; // Nothing in here mentions $this. Declaring a closure inside an // instance method binds it anyway, and curl_setopt parks the // closure on the handle: adapter -> handle -> closure -> adapter. curl_setopt( $this->handle, CURLOPT_WRITEFUNCTION, function ($ch, string $chunk) use (&$body): int { $body .= $chunk; return strlen($chunk); }, ); // ... } } ``` The fix is one keyword: ```php static function ($ch, string $chunk) use (&$body): int { ``` A `static` closure gets no `$this`, so the last edge never forms and refcounting frees the handle as soon as the adapter goes out of scope. Neither callback used `$this` in the first place, which is what makes the change safe and also what makes it so easy to miss. Two file descriptors and roughly a megabyte of native TLS buffers per request, none of it visible to `memory_get_usage()`. Descriptors are capped per process too, so whichever ceiling you reach first decides how the thing dies: the kernel OOMKills you, or you start refusing connections with `Too many open files`. I've written that exact closure before, probably more than once, and it would never have occurred to me that `static` was load-bearing. Adding it to both callbacks in `utopia-php/fetch` took 150 sequential requests from 309 fds and 183 MB RSS to 9 fds and 31 MB, flat. We shipped that and moved on, which was the wrong instinct. It fixed the symptom in one library and left three others still hand-rolling their own transport, each free to reinvent the same bug. ## What the library is `utopia-php/client` is a PSR-18 HTTP client for PHP 8.5. It's about 2,400 lines including both transports, and it doesn't try to be clever: Psr\\Http\\Client\\ClientInterface"] --> B["Utopia\\Clientbase URI · default headers · auth · traceparent"] B --> C["Decorator — Retry, or your own"] C --> D["Adapter"] D --> E["cURL"] D --> F["Swoole coroutine"] `} caption="Every layer implements the same Adapter interface, so they stack in any order and a caller can replace any of them with a stub." /> `Utopia\Client` itself does almost nothing at request time. It resolves the URI against a base, fills in default headers, optionally stamps a `traceparent`, and hands the request to an adapter. Connection lifetime, TLS, timeouts and error classification all live in the adapter. Anything policy-shaped lives in a decorator. ```php $client = new Client(new CurlAdapter()) ->withBaseUri('https://api.stripe.com/v1') ->withBearerAuth($secret) ->withConnectionReuse() ->withTimeout(30); $response = $client->sendRequest( new Request\Factory()->form(Method::POST, 'customers', ['email' => $email]), ); ``` ## Which specs it holds itself to Writing your own HTTP transport in five places means reading the specs badly in five places. Here's what the shared one is on the hook for. | Spec | What it decides | | ----------------------------- | ---------------------------------------------------------------- | | PSR-18 | `4xx`/`5xx` are responses; the two-branch exception contract | | PSR-7 / PSR-17 | Immutable messages and the factories that build them | | RFC 9110 | Idempotency, `Retry-After`, `Authorization`, content negotiation | | RFC 9112 | HTTP/1.1 framing, chunked bodies, status-line parsing | | RFC 9113 | HTTP/2, which APNs requires and HTTP/1.1 can't satisfy | | RFC 3986 | Base-URI resolution and dot-segment removal | | RFC 6750 / 7617 | `Bearer` and `Basic` credential formats | | RFC 7578 / 2046 / 2183 | `multipart/form-data`, boundaries, `Content-Disposition` | | RFC 1951 / 1952 / 7932 / 8878 | deflate, gzip, br, zstd content codings | | RFC 8446 | TLS 1.3, and the floor you can pin below it | | W3C Trace Context | `traceparent` propagation | ### PSR-18 decides what counts as an error The sentence that does the most work: > A Client MUST NOT treat a well-formed HTTP request or HTTP response as an error condition. For example, response status codes in the 400 and 500 range MUST NOT cause an exception and MUST be returned to the Calling Library as normal. A `429` is not a failure, it's an answer. So `sendRequest()` returns it, and only genuine "there is no response" conditions throw. PSR-18 splits those into two branches: `RequestExceptionInterface` for a malformed request or response, `NetworkExceptionInterface` for a transport that failed. The type answers one question, which is the only one you have at the catch site. Would trying again help? The library's own hierarchy keeps that property all the way down. ```text ClientExceptionInterface ├── NetworkExceptionInterface — retrying may help │ └── NetworkException │ ├── DnsException, TimeoutException, ProtocolException, ProxyException │ └── ConnectionException │ └── TlsException └── RequestExceptionInterface — retrying is pointless └── RequestException ├── InvalidUriException, InvalidResponseException └── AdapterPreconditionException, AdapterInitializationException ``` Each adapter maps its native error codes into that tree. The cURL adapter matches on `CURLE_*` constants, guarded by `defined()` so a libcurl build without HTTP/3 doesn't fatal at load. Another line from the spec you can find in the code almost verbatim: > If a Client chooses to decompress the message body then it MUST also remove the `Content-Encoding` header and adjust the `Content-Length` header. Both adapters negotiate compression for you: the request advertises whatever codecs the transport can decode, and the response arrives as plaintext. Which means the `Content-Encoding: gzip` and `Content-Length` the server sent are now lies about the body you're holding, so the adapter drops both. Set your own `Accept-Encoding` and it gets out of the way entirely. ### Retrying, and why only some requests get to `Retry` is a decorator, and its default `Backoff` strategy reads almost directly off RFC 9110. Only idempotent methods (§9.2.2) are retried, so a lost response can't turn into a double charge. Only transient outcomes are retried: a `NetworkExceptionInterface`, or a `429` / `502` / `503` / `504`. A numeric `Retry-After` (§10.2.3) beats the computed delay, because the server knows things the client doesn't. With no `Retry-After`, the wait is exponential with full jitter: a value drawn uniformly from `[0, ceiling)` rather than the ceiling itself. Our workers run as a fleet. A fleet that backs off deterministically comes back at a struggling upstream in lockstep and keeps it struggling. Every one of those decisions lives behind a single method, so a library with different rules writes its own: ```php interface Strategy { public function delay( RequestInterface $request, int $attempt, ?ResponseInterface $response, ?ClientExceptionInterface $error, ): ?float; } ``` `utopia-php/storage` does exactly that. S3 signals throttling in an XML body as often as in a status code, so `S3\RetryStrategy` parses the body first and retries `SlowDown`, `ServiceUnavailable`, `Throttling` and `RequestThrottled`. It also refuses to retry a `503` whose body parses cleanly into some other error code, which is the case a status-code-only rule gets wrong. ### `withBaseUri()` is not string concatenation It looks like a convenience until you send `../v2/users` and find out which one your client implements. This one does dot-segment removal, and only applies the base when the request URI is actually relative. An absolute URI passes through untouched. ### Everything else is a header you'd otherwise hand-roll `withBasicAuth()` is RFC 7617's `base64(user:pass)`. `withBearerAuth()` is RFC 6750's `Bearer `. `Part::file()` builds an RFC 7578 part with its RFC 2183 `Content-Disposition`. `withMinTlsVersion(Tls::V1_2)` is an enum each adapter maps to `CURLOPT_SSLVERSION` or Swoole's `ssl_protocols`. `withTracePropagation()` forwards the active `utopia-php/span` trace as a `traceparent`, and refuses to overwrite one that's already on the request. None of these are hard. They're just wrong in slightly different ways in every library that rolls its own. ## The four rules underneath ### Reuse over recreate `withConnectionReuse()` keeps one connection alive per client and reuses it for every request to the same origin. `curl_reset()` clears per-request options while preserving the handle's connection cache; the Swoole adapter keeps a kept-alive coroutine client keyed by origin. It's opt-in, on the theory that a client built for one call shouldn't sit on a socket. Any long-lived service wants it on. For Pay, that one change was the entire fix. Four hundred requests against a local echo server, before and after: | | fds | RSS | | ----------------- | -------- | ---------- | | client per call | **+800** | **+21 MB** | | reused connection | **+0** | **+48 kB** | When you need concurrency rather than sequence, `Client\Pool` borrows a client from a `utopia-php/pools` pool per request and reclaims it afterwards, so N coroutines share a bounded set of connections instead of opening N of their own. ### Every `with*()` returns a clone In Swoole that's not a style preference. A shared client you can mutate is a cross-request bleed waiting to happen, and cloning means there's no way to reconfigure someone else's client from inside a request handler. A default is also only ever a default. `withHeaders()` fills in a header the request doesn't already carry and nothing more, so a per-request `Content-Type` beats the client-wide one. ### Policy lives in decorators `Retry` implements the same `Adapter` interface it wraps, forwards every configuration helper inward, and overrides only `sendRequest()` and `stream()`. So retries, pooling and whatever you add stack in any order, and none of them turn into constructor flags on the transport. The `stream()` override is my favorite detail in the library. It counts bytes handed to the sink, and once a single byte has been delivered it stops retrying, because replaying would duplicate data the caller already processed. ### Bounded memory by default `stream()` hands each chunk to a sink as it arrives, so SSE and LLM token streams cost the same memory as a ping. Uploads go the same way: cURL pulls the body through a read callback, `Part::file()` reads lazily, and Swoole sends files with zero-copy `sendfile()`. A seekable body gets rewound before each attempt, which is what makes a streamed upload safe to retry at all. ## Where it ended up | Package | Before | Now | | ---------------------- | ----------------------------- | ----------------------------------------------------------- | | `utopia-php/pay` | `new Fetch\Client` per call | one injected client, `Adapter::call()` deleted | | `utopia-php/messaging` | raw `curl_*` and `curl_multi` | PSR-18, plus a Swoole pool for batched FCM/APNs | | `utopia-php/storage` | ad-hoc HTTP in the S3 devices | default client with a stall watchdog and `S3\RetryStrategy` | | `utopia-php/fastly` | — | built on it from day one | | `appwrite/appwrite` | per-service HTTP wiring | `jobs` and `screenshots` clients in the DI container | The Pay migration was net **-55 lines**. `Adapter::call()`, `handleError()`, and nine `METHOD_*` constants went with it — seven of the nine had no caller. A PSR-18 client and a PSR-17 factory already mean "build a request, send it"; the indirection was only ever a second vocabulary for HTTP. Messaging is the one worth reading if you're doing this yourself. It kept every adapter's public API identical while swapping `curl_multi` for Swoole coroutines over a bounded pool, and added a `Closure(): ClientInterface` factory so a caller can inject retries, a proxy, or a stub. The one constraint it documents loudly: your factory must produce a client that can negotiate HTTP/2, because APNs rejects HTTP/1.1 outright. ## What we grep for now - Climbing RSS with a flat PHP heap. Count fds, not `memory_get_usage()`. `ls /proc//fd | wc -l` in a loop finds this in five minutes. - `new Client()` or `curl_init` inside a request path. In a long-lived process a client is a resource with a lifetime, not a local variable. - Non-static closures handed to `curl_setopt`. If it doesn't touch `$this`, make it `static`. Costs nothing, and it's the whole bug. - Any utopia library still carrying its own private HTTP stack. That's the rest of the work list. Repo: [utopia-php/client](https://github.com/utopia-php/client), though development happens in the [monorepo](https://github.com/utopia-php/monorepo/tree/main/packages/client). [@lukebsilver](https://twitter.com/lukebsilver) wrote it and did the Pay migration. I did messaging, which is how I ended up with opinions about APNs. fetch(...) per Stripe API call, with a paste showing rss=63212kB fds=7 at start and rss=64696kB fds=47 at i=40, exactly +1 fd per fetch, RSS monotonic, PHP heap flat at 6.0MB throughout. Solution is to migrate to utopia-php/client. Reply: the astronaut meme, wait, solution is migrate to utopia-php/client? always has been." caption="Internal consensus, after the third identical incident. The paste is the diagnostic: fds climb one per call, RSS follows, the PHP heap never moves." /> ## Reference | Term | What it means here | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | file descriptor (fd) | The integer the kernel gives a process to refer to something it has open. Every live socket holds at least one, and each process has a cap (`ulimit -n`). | | RSS | Resident set size: the physical memory a process actually occupies, native allocations included. This is what the kernel measures when it decides to kill you. | | OOMKilled | The kernel terminating a process for exceeding its memory limit. It happens outside PHP, so there's no fatal error and nothing in the logs. | | PHP heap | The pool the Zend allocator manages, which is what `memory_get_usage()` reports and `memory_limit` caps. cURL and OpenSSL allocate outside it. | | reference cycle | Objects holding each other so no refcount ever reaches zero. Only PHP's cycle collector can free them, and it runs on its own schedule. | | idempotent method | A request that can be sent more than once without changing the outcome. `GET`, `HEAD`, `PUT`, `DELETE`, `OPTIONS`, `TRACE` qualify; `POST` doesn't. | | full jitter | Backoff where the wait is drawn uniformly from `[0, ceiling)` instead of being the ceiling, so a fleet retrying together spreads out. | | coroutine | Swoole's userland concurrency. One worker process interleaves many in-flight requests, which is why connection lifetime matters so much. | | sink | The callback `stream()` hands each response chunk to as it arrives, instead of buffering the whole body. | | APNs / FCM | Apple and Google's push notification services. APNs is the one that rejects HTTP/1.1. | ### The specs - HTTP semantics in [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110), message framing in [RFC 9112](https://www.rfc-editor.org/rfc/rfc9112), HTTP/2 in [RFC 9113](https://www.rfc-editor.org/rfc/rfc9113), and `429` in [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585) - PHP-FIG: [PSR-18](https://www.php-fig.org/psr/psr-18/) for clients, [PSR-7](https://www.php-fig.org/psr/psr-7/) for messages, [PSR-17](https://www.php-fig.org/psr/psr-17/) for factories - URIs in [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986); credentials in [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750) and [RFC 7617](https://www.rfc-editor.org/rfc/rfc7617) - Multipart in [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578), building on [RFC 2046](https://www.rfc-editor.org/rfc/rfc2046) and [RFC 2183](https://www.rfc-editor.org/rfc/rfc2183) - Content codings: [deflate](https://www.rfc-editor.org/rfc/rfc1951), [gzip](https://www.rfc-editor.org/rfc/rfc1952), [br](https://www.rfc-editor.org/rfc/rfc7932), [zstd](https://www.rfc-editor.org/rfc/rfc8878) - [TLS 1.3](https://www.rfc-editor.org/rfc/rfc8446) and [W3C Trace Context](https://www.w3.org/TR/trace-context/) ### The code - [utopia-php/client](https://github.com/utopia-php/client), developed in [the monorepo package](https://github.com/utopia-php/monorepo/tree/main/packages/client), on top of [utopia-php/psr7](https://github.com/utopia-php/psr7) and [utopia-php/pools](https://github.com/utopia-php/pools) - [fetch#22](https://github.com/utopia-php/fetch/issues/22) — the reference cycle, with the fd and RSS measurements - [pay#32](https://github.com/utopia-php/pay/pull/32) — the migration that fixed the OOMKills, and the before/after numbers - [messaging#137](https://github.com/utopia-php/messaging/pull/137) — `curl_multi` swapped for Swoole coroutines over a pool ### Background - AWS, [Exponential Backoff and Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/), which is where the full-jitter formula comes from - PHP manual, [Collecting Cycles](https://www.php.net/manual/en/features.gc.collecting-cycles.php), on when the collector actually runs --- ## MCP 2.0: the release that deleted the handshake **Slug:** mcp-2-0-the-release-that-deleted-the-handshake MCP versions are dates. `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, and since two weeks ago, `2026-07-28`. From the outside they all look like the same kind of event. The last one isn't. The working group called the release candidate "the largest revision of the protocol since launch," which usually means a long changelog. Here it means the connection model is gone. No `initialize`. No session id. No held-open stream. If you'd asked me a year ago to describe MCP in one sentence, that sentence would be wrong now. Nothing you have deployed today stops working, which is exactly why it's easy to skim past. ## The handshake era Every MCP revision up to and including `2025-11-25` opened the same way. The client sends `initialize` with its protocol version, its capabilities and its `clientInfo`. The server answers with the version it picked, its own capabilities and its `serverInfo`. The client sends `notifications/initialized`. Only then is the connection allowed to carry anything useful. That exchange establishes a session, and everything after it is defined relative to that session. Over Streamable HTTP the server could mint an `Mcp-Session-Id`, which the client then echoed on every request; `DELETE` ended it, `Last-Event-ID` resumed a dropped SSE stream. The design makes complete sense if you remember where MCP started. In November 2024 it was a thing you ran as a subprocess of your editor over stdio. One client, one server, one pipe, and the pipe's lifetime _is_ the session. Negotiating capabilities once at the start costs nothing when there is exactly one of everything. Streamable HTTP arrived in `2025-03-26` and carried that model onto the network, where "the connection has a memory" stops being free. So you pick one: sticky routing, or a shared session store. Same problem, different bill. Every deploy drops live sessions. A crashed replica takes conversations with it. And since the session id tells a gateway nothing about what the request wants, the gateway has to open the JSON body to find out whether it's metering a `tools/list` or a database drop. stdio, onesubprocess"] --> B["2025-03-26Streamable HTTP+ OAuth"] B --> C["2025-06-18elicitation,structured output"] C --> D["2025-11-25tasks, icons,CIMD"] D --> E["2026-07-28no session,no handshake"] `} caption="Four revisions of adding things to a connection, then one that removed the connection." /> ## What 2026-07-28 actually does **The handshake is gone.** No `initialize`, no `initialized`, no `Mcp-Session-Id`. Everything that used to be negotiated once now travels on every request, in `_meta`. The same tool call, before and after: The second one is cold. That client has never spoken to this server, nothing was set up first, and nothing is kept afterward. The three new headers are there so a gateway can route and meter it without opening the body. Version negotiation stopped being a phase and became a field. The server accepts or rejects each request on its own, and if it can't speak the version it answers `UnsupportedProtocolVersionError` listing the versions it can, so the client retries. There's a `server/discover` method that returns supported versions and capabilities in one call, which servers **MUST** implement and clients don't have to call. Discovery is a convenience now, not a precondition. **Server-to-client calls stopped needing an open pipe.** That was the load-bearing dependency on sessions. A tool that needs to ask the user something used to do it over a stream held open for the length of the call, which pins the call to one process. Under SEP-2322 the server returns an `InputRequiredResult` and hangs up. The client answers later with `inputResponses` and the `requestState` it was handed, and any instance can take it from there. SEP-2260 tightens things further: a server may only start a request while it's actively processing one from the client. Between them they replace `elicitation/create` and `sampling/createMessage`. State moved into the open. The spec is blunt about where it went: > Servers that need to carry state across calls can do what HTTP APIs have always done: mint an explicit handle (a `basket_id`, a `browser_id`) from a tool and have the model pass it back as an ordinary argument on later calls. The spec's own phrasing is that this "makes the state visible to the model rather than hidden away." A session was state the model never saw and could not recover once it was gone. **Infrastructure can read the traffic without parsing it.** | SEP | What it adds | What it buys | | ---- | --------------------------------------------------- | ------------------------------------------------------------------- | | 2243 | `Mcp-Method` and `Mcp-Name` request headers | Routing, metering and WAF rules with no body inspection | | 2549 | `ttlMs` and `cacheScope` on list and read | Clients know how long a tool list is good for, and who may share it | | 414 | `traceparent` / `tracestate` / `baggage` in `_meta` | One trace across client, gateway and server, into any OTel backend | A server rejects any request where the headers and the body disagree, which is what makes the headers safe to trust for anything that never looks inside. ## Why this is 2.0 and not 1.4 The protocol changes are the part you notice. The governance changes are the part that decides whether any of it survives the next revision, and there are three. Extensions became first class. They get reverse-DNS identifiers, negotiate through an `extensions` map, version independently of the spec, and live in their own `ext-*` repositories with their own maintainers. Tasks graduated out of the core and into `io.modelcontextprotocol/tasks`. MCP Apps, which is server-rendered HTML in a sandboxed iframe talking back over the same JSON-RPC, ships as one too. The core stays small and the interesting parts move at their own speed. Features now have a lifecycle: Active, Deprecated, Removed, with a documented migration path and twelve months minimum before anything deprecated is eligible for removal. Three features entered it immediately. | Deprecated | Use instead | | ---------- | -------------------------------------------------------- | | Roots | Tool parameters, resource URIs, or server configuration | | Sampling | Your LLM provider's API, directly | | Logging | `stderr` on stdio, OpenTelemetry for anything structured | All three still work. That's the point of writing the policy down. Conformance became a gate. A Standards Track SEP can't reach Final until matching scenarios land in the conformance suite, and official SDKs are scored against that suite under a tier system. All four Tier 1 SDKs (TypeScript, Python, Go, C#) shipped `2026-07-28` support the day the spec did, with Rust in beta. The remaining breaking changes are small but worth knowing. Tool schemas moved to full JSON Schema 2020-12, so `oneOf`, `$ref` and conditionals are legal now, and the spec says out loud that you must not auto-dereference external `$ref` URIs. `structuredContent` takes any JSON value instead of only objects. The missing-resource error went from MCP's custom `-32002` to the standard `-32602`, which breaks anyone matching on that literal. Every `2025-11-25` Tasks implementation needs migrating. ## We had already bet on this I spent sixteen months building [Appwrite's MCP server](https://github.com/appwrite/mcp), and [most of that time went to authorization](/blog/how-i-built-the-appwrite-mcp-server). When I got to the transport, the hosted server went out with one flag set: ```python StreamableHTTPSessionManager(app=server, json_response=False, stateless=True) ``` No sessions. Every request carries a bearer token; verify it, build a client from it, serve the call, forget everything. I'd love to claim foresight here. The real reason is duller: OAuth had already made session state pointless, because the token identifies the user on every request, and I didn't want to run a Redis to hold something I had no use for. The spec shipped on July 28. We merged `2026-07-28` support on July 29, in [PR #89](https://github.com/appwrite/mcp/pull/89), by moving from MCP Python SDK v1.28 to v2.0.0. The HTTP and OAuth surface didn't move at all. What made it a day rather than a quarter is that v2's session manager routes on the protocol version header, so one deployment serves both eras: ```python def _is_modern_request(scope: Scope) -> bool: """Match the SDK's era-routing predicate: a present ``MCP-Protocol-Version`` that is not a handshake-era revision goes to the modern path.""" version = _header(scope, b"mcp-protocol-version") return version is not None and version not in HANDSHAKE_PROTOCOL_VERSIONS ``` Handshake versions, `2024-11-05` through `2025-11-25`, take the legacy path. Anything else takes the per-request one. Nobody's client broke. Three things did cost real work: - **Tool errors.** v1 wrapped a raised exception into `CallToolResult(isError=true)`. v2 turns an uncaught exception into a sanitized `-32603`, so the model never learns why the call failed. Our `confirm_write` refusals are only worth anything if the model can read them, so every handler now returns `is_error=True` explicitly. - **CORS.** `Mcp-Method` and `Mcp-Name` ride on every modern request. If your allow-list doesn't name them, browser-based clients fail before they ever reach your auth code. - **Metrics.** Modern clients never send `initialize`, so our handshake counter would have quietly flatlined exactly as clients migrated, which is the worst possible time to lose a graph. It's keyed off activity windows now, so the timeseries stays comparable through the transition. None of that is protocol work. It's the ordinary cost of a dependency changing shape under you, and it only took a day because a decision made eighteen months earlier, for completely unrelated reasons, happened to line up. ## The good kind of boring A `2026-07-28` MCP server takes an HTTP request, reads some headers, and returns a response. That's the whole shape of it. You can put it behind a CDN, cache its list endpoints, and restart it in the middle of someone's conversation without telling anyone. It's a less interesting protocol than the one that launched in 2024. I think that's the point. The bidirectional stateful version was more fun to read about and considerably worse to operate, and operating it is what most of us actually do. ## Reference | Term | What it means here | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | handshake era | Every MCP revision through `2025-11-25`, where a connection began with `initialize` and carried a session from then on. | | `Mcp-Session-Id` | The header that identified that session. Removed in `2026-07-28`. | | `_meta` | A metadata object on every request. Now carries protocol version, client info and capabilities, one copy per request. | | `server/discover` | A single call returning supported versions, capabilities and identity. Mandatory for servers, optional for clients. | | sticky routing | Making a load balancer send every request from one client to the same backend. The price of server-side session state. | | `requestState` | The opaque blob a server hands back with an `InputRequiredResult` so the follow-up can be processed by any instance. | | SEP | Specification Enhancement Proposal, MCP's change process. Standards Track SEPs now need conformance scenarios before reaching Final. | | Streamable HTTP | The HTTP transport introduced in `2025-03-26`, replacing the original HTTP+SSE one. | | CIMD | Client ID Metadata Documents, the replacement for Dynamic Client Registration in OAuth flows. | ### The spec - The [release candidate announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) and the [release itself](https://blog.modelcontextprotocol.io/posts/2026-07-28/) - [`2026-07-28`](https://modelcontextprotocol.io/specification/2026-07-28/), [versioning and negotiation](https://modelcontextprotocol.io/specification/versioning), and [`server/discover`](https://modelcontextprotocol.io/specification/2026-07-28/server/discover) - The [feature lifecycle policy](https://modelcontextprotocol.io/community/feature-lifecycle) and the [deprecated features registry](https://modelcontextprotocol.io/specification/2026-07-28/deprecated) - The [`2025-11-25` changelog](https://modelcontextprotocol.io/specification/2025-11-25/changelog), for what the last handshake-era revision was still adding ### The code - [appwrite/mcp](https://github.com/appwrite/mcp), and [PR #89](https://github.com/appwrite/mcp/pull/89) — the dual-era migration - The hosted server is at `https://mcp.appwrite.io/`, or `claude mcp add --transport http appwrite https://mcp.appwrite.io/` --- ## Seven months of coding agents: what $28,335 actually bought **Slug:** seven-months-of-coding-agents Seven months, $28,335, across five agent harnesses. For a lot of people running agents right now that's a heavy week. I publish that number on [/trends](/trends) and it updates itself, so I've had a while to look at it. What I expected to find was a story about cost. What's actually in the data is a story about scheduling, and the number that tells it is this one: **0.7% of my agent's tool calls happened between midnight and six in the morning.** Claude Code and Codex arrive at 0.7% independently. Different vendors, different session formats, 1,432 sessions between them, and I only noticed the match because I ran the same script twice and thought I'd double-counted. ## The shape of the bill That's 201,473 tool calls bucketed by the hour I was awake for them. It looks like a workday because it is one. The peak is 16:00. The trough is 03:00, and the trough is empty. An agent that runs unattended doesn't produce this shape. If you leave loops running, your histogram flattens — the overnight band fills in, because that's the entire point of leaving them running. Mine has a hole in it, and the hole is where I was asleep. The weekly view says the same thing. Weekdays average $176 a day; weekends average $67. The spend tracks my calendar, not a scheduler's. ## What the spend actually bought The curve is boring until April, where it goes up 7.4× in one month and then stays there. That's not a pricing change and it isn't a new model. April is when I stopped running one conversation at a time and started running agents in parallel git worktrees, which is the single largest change in how much I spend and the single largest change in how much lands. Underneath it: 9,350 commits across 145 repositories since January, concentrated in `sdk-generator`, `cloud` and `appwrite`. I want to be careful with that number, because it's the one most likely to be wrong in my favour. `sdk-generator` emits bulk generated output, so its 1,933 commits are not 1,933 units of thought. And only 205 commits carry a `Co-authored-by` trailer for an agent, which is not a measure of anything except how inconsistently I remember to keep the trailer. Take 9,350 as a statement of scale and nothing finer. ## The leash is fifteen actions long The interesting ratio isn't dollars per commit. It's how much rope the agent gets before I say something. Fifteen actions, then I say something. That is the whole working relationship in one number, and it has stayed roughly constant while the spend went up 300×. What April changed was how many leashes I hold at once, not how long any one of them is. The interrupt count is the part I didn't expect. 2,047 times I stopped a run in progress — a little over ten times per active day. I'd have guessed a tenth of that. Interrupting is so cheap that it stopped registering as an event, which is a strange thing to learn about your own habits from a log file. 296 of 1,432 sessions were single-turn: ask, receive, done. That's 21%. The other 79% are conversations. The median session is four to seven human turns over sixteen to thirty-four minutes, which is not automation. It's pairing, with a partner who types faster than me and needs checking. ## The part that argues against me I have around 130 skills installed. Across all my Claude Code transcripts, they've been invoked 52 times. Twenty-six of those are one skill, `release-sdk`, which is a genuine repetitive chore. After that: `artifact-design` six times, `dataviz` four, `sentry-cli` three, and then a tail of skills fired exactly once. The infrastructure behind those 130 is not casual. There's a harness-neutral root at `~/.agents/skills` with a lock file that pins each skill to a GitHub commit, symlinked outward into Claude Code, Codex and Pi, with a config listing fifteen harnesses it knows how to install into. `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md` and `~/.config/opencode/AGENTS.md` are byte-identical, all three md5 `295f62fc`. I built a distribution system for my own instructions. And then I mostly don't invoke the things it distributes. I type instead. There are two readings. The generous one is that skills are a library, and a library you consult four times a year is still worth having. The honest one is that I enjoy building the workshop more than I use the tools in it, and that 130-installed-52-fired is a tooling habit rather than a workflow. Both are probably true. What neither reading supports is the idea that any of this productivity came from automation running without me. ## The choice, and what it costs Here's the objection I can't fully answer: is 0.7% restraint, or just circumstance? Right now the marginal cost of running agents overnight is about as low as it will ever be. Everything is subsidised. I have the budget, five harnesses configured, worktree isolation already working, and a skills system that can install into fifteen agent runtimes. Every ingredient for a 24/7 loop is sitting on this machine, and the histogram still has a hole in it. So it's a choice, and it's worth being clear about what I'm buying with it. Fifteen tool calls is roughly how much work I can hold in my head and still meaningfully review. Past that I'm not reviewing, I'm accepting. The 2,047 interrupts are the mechanism: they only happen because I'm watching, and I'm only watching because the run is short enough to watch. The cost is real. There's work I could have shipped overnight and didn't. The trade is that I can still explain every one of those 9,350 commits, which matters more to me in a codebase I'll be on call for. $28,335 over seven months is a modest bill by current standards. I don't think that's because I was careful with money. I think it's because a leash fifteen actions long is inherently cheap, and the thing it's actually buying is that I never stopped being the one holding it. --- ## Will MCP replace the CLI? **Slug:** will-mcp-replace-the-cli Every developer tool now seems to need an MCP server. The growth is real: monthly downloads of the MCP TypeScript SDK went from 1.9 million in March 2025 to 191.9 million in July 2026. But the command line is not collapsing underneath it. Downloads of Commander, a widely used Node.js CLI framework, grew from 787 million to 1.93 billion over the same period. The numbers are imperfect because npm downloads include CI and transitive installs. Still, they look less like a replacement and more like a new layer being added. ## MCP is not the GUI for agents A GUI and CLI are interfaces for humans. MCP is a contract between a model host and software. The host discovers tools, gives their schemas to the model, validates arguments, and sends the call. An agent can already use a CLI. It can run `--help`, execute a command, inspect stdout, and retry. MCP makes that operation typed, discoverable, and easier for the host to police. That difference is enough to split the market: | MCP wins when… | CLI wins when… | | -------------------------------------- | ----------------------------------- | | The product is remote | The work is local | | OAuth should happen in the client | Credentials already exist | | Actions need a rendered confirmation | Commands need to run unattended | | The agent must discover capabilities | The workflow must be reproducible | | Structured output matters more than UX | Pipes, files, and exit codes matter | For a SaaS product, MCP is a much better front door. One URL and an OAuth flow can replace installing a package, setting environment variables, and teaching the model a command tree. O["OAuth"] O --> D["Discover tools"] D --> C["Confirm call"] `} caption="MCP turns installation, authentication, discovery, and approval into one client workflow." /> For repository work, the calculation reverses. `git diff`, `bun test`, and `docker compose logs` are already available, inspectable, and easy for a human to rerun. Wrapping each in a tool server often adds schema without adding a useful boundary. ## The MCP trend nobody advertises Early MCP servers tried to expose everything. Clients and models pushed back. Cursor once sent only the first 40 tools to the agent; Windsurf refused servers that crossed its limit. The current direction is smaller surfaces with search behind them. Appwrite went from 981 methods to four visible tools. Stripe and Sentry also put larger catalogs behind search; GitHub groups its tools into sets. That is a real drawback, not an implementation detail. MCP tools consume context merely by existing. A CLI can have hundreds of subcommands without pasting every help page into the conversation. Uploads expose another mismatch. A hosted MCP server cannot read a path on the user's machine. Appwrite prefers a public URL, but also accepts a small image or deployment ZIP as Base64 inside the JSON tool call. Decoded inline content is capped at 10 MB, and Base64 makes the payload roughly one-third larger. The Base64 path works, but it is a hack around the transport. The CLI can open `./site.zip` and upload its bytes directly without making the artifact public or passing it through the model's context. MCP also gives you another service to operate: authorization, deployment, observability, client compatibility, schema versioning, and a security policy for every mutation. OAuth proves who the user is. It does not prove the model understood what the user wanted before calling `delete_project`. The CLI's problems are older and easier to see: prose output, shell quoting, broad environment access, platform differences, and weak capability discovery. It is a worse interface for a model, surrounded by a better ecosystem for debugging and automation. ## My prediction CLI A["Agent host"] --> MCP A -. "sandboxed shell" .-> CLI CLI --> API MCP --> API `} caption="MCP does not need to replace the CLI to win. It only needs to become the agent-facing adapter." /> MCP will become the default way an agent first meets a remote product. The CLI will remain the deterministic path for local work, scripts, CI, and the moment an abstraction leaks. So I do not expect a replay of “GUI beats terminal.” That verdict never happened. Most people chose GUIs; developers kept terminals for the work where precision and composition mattered. MCP can win distribution without winning everything. The durable stack is likely an API underneath, a CLI for operators, and a narrow MCP surface for agents. --- ## Appwrite MCP vs Vercel MCP: I benchmarked both **Slug:** appwrite-mcp-vs-vercel-mcp An MCP server should be judged by what an agent can finish, not by how many tools sit in its catalog. So I pointed an agent at the hosted Appwrite and Vercel MCP servers and asked both to do the same job: deploy identical applications, inspect the builds, diagnose a deliberate failure, ship a second version, and recover from it. The two servers are built for different jobs, and the result splits along those lines: - **Appwrite reached much further into the backend** and was the only one of the two exposing rollback, deletion, and explicit write confirmation through MCP. - **Vercel deployed faster and reported runtime failures better.** It reached `READY` in about 2.1 seconds on the static app and its dedicated log tools filter properly. ## How I tested Both hosted MCP servers ran over OAuth from the same Darwin 25.5 arm64 client. I threw away the first call to each tool so nobody paid for a cold start, then alternated between the two servers so neither got the benefit of going second. Anything that wrote data went into a throwaway project I could delete afterwards. Two test apps: - A static site with identical `index.html` and `marker.json` files, deployed from the same source archive on both platforms. - A Next.js 15.2.8 SSR app with deterministic build markers and controlled 200, 400, and 500 responses. Each agent had to discover its workspace, deploy both apps, inspect logs and analytics, recover from an intentional build failure, publish v2, attempt a rollback, and handle 50 requests at concurrency 10. ### How I scored it Six things counted, correctness most of all: correctness and completeness at 30%, then result compactness, discoverability and call economy, median and p95 latency, and safety and lifecycle control at 15% each, and error recovery quality at 10%. The two combined totals landed close enough that the gap between them tells you nothing, and the per-category numbers behind them were never written up. The per-task scores from the read and observability phases are the ones worth putting on screen: Appwrite wins five of the eight tasks, and the three it loses are the ones it loses badly. ## Router versus toolbox Vercel exposes **33 direct tools** covering projects, deployments, logs, analytics, agent runs, collaboration, domains, purchases, and deployment protection. Once the agent has a team or project ID, it calls a tool named for the task. Appwrite exposes **992 operations across 81 services** behind a handful of meta-tools. Instead of loading every schema into the model's context, the agent works through four calls: - `appwrite_get_context` to find the account, organization, and project - `appwrite_search_tools` to find an operation - `appwrite_call_tool` to execute it - `appwrite_search_docs` to search current documentation The router usually costs one extra call before an unfamiliar operation, and buys access to Auth, Databases, Storage, Messaging, Functions, Sites, usage, domains, and infrastructure from one interface. Vercel's toolbox is easier to navigate as long as the work stays inside deployment. ## What each server actually covers ## Lifecycle control: Appwrite exposed more Getting the first URL live is the easy half. An operator also has to publish updates, switch versions, disable things, and clean up. Appwrite deployed v2 to the existing Site with an explicit build cache hit, switched the active deployment, and then rolled back to v1 in **1.32 seconds**. The public health endpoint returned `version: "v1"` immediately. Disabling the Site returned a 404 `router_deployment_not_found`, and re-enabling restored v1 straight away. Vercel's inline deployment tool created a _new project_ for v2, which also meant it reported no previous build cache. Its tested MCP surface exposed no rollback, no redeploy to an existing project, no project deletion, and no general environment-variable management. Those capabilities exist elsewhere on the Vercel platform; the benchmark required MCP-only operation. The practical consequence is that both Vercel projects from this run are still live, because nothing in the 33 tools can delete them. Write safety differed too. Appwrite required `confirm_write=true` before every mutation and refused the first unconfirmed create. Vercel's deploy tool had no equivalent parameter, though its commercial operations do separate quotes from purchases. ## Deployment speed: Vercel was a bit faster Vercel accepted an inline file tree and had the static app ready in about **2.1 seconds**, on one write. Appwrite needed two, create the Site then upload and activate a gzipped deployment, and took about **27 seconds end to end**. On SSR the gap narrowed to **39.1 seconds** against a **91-second build**, roughly 2.3 times faster on reported build time alone. Both platforms rejected an earlier version of the SSR app, for different and legitimate reasons: Appwrite's adapter expected a `next.config.*` file, and Vercel blocked Next.js 15.2.4 over a known vulnerability. Each agent diagnosed its own failure from the logs it was handed. ## Observed latency Once the agent had picked a project, Appwrite was faster on every repeated read I measured, in the case of build logs by a factor of eight. Both servers cleared the bounded concurrency check without errors. Appwrite returned 50 of 50 in **5.146 seconds** wall time, Vercel 50 of 50 in **4.714 seconds**. Vercel was modestly faster, and the sample is far too small to support a broader performance claim. Compactness is where the read gap is real rather than marginal. Asking for 100 log entries returned roughly 115 KB from Appwrite for 96 events, which got context-truncated, against 12.3 KB of filtered events from Vercel. ## Choosing between them **Appwrite MCP** fits when the agent has to operate the backend as well as the deployment: users, data, files, messaging, functions, and Sites. It is also the stronger fit when you need explicit write confirmation, rollback, repeated deployment to one resource, and cleanup the agent can actually perform. **Vercel MCP** fits when the job is deploying, inspecting, diagnosing, and analyzing Vercel-hosted applications. Its direct tools remove selection ambiguity, its builds were faster here, and its log filters make for a cleaner diagnostic loop. Worth saying plainly: both of these deploy sites well. Every app I handed to either server went live, and both agents read their own build failure and fixed it without help. Vercel got there faster, 2.1 seconds against 27 on the static app and roughly 2.3 times quicker on the SSR build, and its log responses came back small enough to actually read. Appwrite was slower to the first URL and reached much further once it arrived: rollback, deletion, write confirmation, and the rest of the 992 operations behind the router, none of which the 33 Vercel tools touch. If your work stops at deploying, that speed is the whole story. If it doesn't, one extra search call buys you a lot. --- ---