How the Internet Actually Works: Understanding Client-Server Architecture with Real Code

How the Internet Actually Works: Understanding Client-Server Architecture with Real Code

The Big Picture: What Happens When You Visit a Website?

Every time you type a URL into your browser and hit Enter, a surprisingly complex chain of events kicks off in the background. Most people never think about it, but understanding this process is the foundation of everything we’ll cover in this guide.

Let’s walk through what actually happens when you visit something like https://www.example.com.

Step 1: Your browser looks up the address

Your computer doesn’t understand domain names like www.example.com. It needs an IP address, which is basically a numerical home address for a server somewhere in the world. To get it, your browser contacts a DNS (Domain Name System) server, which works like a giant phone book for the internet. It translates the human-friendly name into something like 93.184.216.34.

Step 2: Your browser opens a connection

Now that your browser knows the IP address, it reaches out to that server and says “hey, I’d like to talk.” This happens using a protocol called TCP (Transmission Control Protocol), which sets up a reliable back-and-forth channel between your machine and the server.

Step 3: Your browser sends an HTTP request

Once the connection is open, your browser sends a message to the server. That message looks something like this:

GET / HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

This is a raw HTTP request. It’s just text, following a specific format. The GET part means “please give me this resource.” The / refers to the homepage.

Step 4: The server sends back a response

The server receives your request, figures out what you want, and sends back a response:

HTTP/1.1 200 OK
Content-Type: text/html

<!DOCTYPE html>
<html>
  <body><h1>Hello, World!</h1></body>
</html>

The 200 OK means everything went fine. The server then sends the actual HTML content your browser will display.

Step 5: Your browser renders the page

Your browser reads the HTML, fetches any additional files it needs (like CSS, images, or JavaScript), and paints the final page on your screen.

The whole process typically takes under a second, but it involves your computer, at least one DNS server, and a web server potentially located on the other side of the planet.

This request-and-response cycle is the heartbeat of the web. Everything else we cover in this tutorial builds directly on top of it.

Clients vs. Servers: Who Does What?

Before we write any code, let’s get one thing straight: a client and a server are just two computers (or programs) having a conversation. That’s it. No magic involved.

Here’s the simple breakdown:

  • The client is the one asking questions. Your web browser is a client. When you type a URL and hit enter, your browser is saying “Hey, can I have that webpage?”
  • The server is the one answering. It sits around waiting for requests, and when one arrives, it figures out what to send back.

Think of it like ordering food at a restaurant. You’re the client, you make a request (“I’ll have the burger”), and the kitchen is the server, preparing and sending back exactly what you asked for.

A Client Sends Requests

A request has a few key pieces:

  1. A method (like GET or POST) that says what kind of action you want
  2. A URL that says where you want to do it
  3. Headers that carry extra info (like what kind of data you can accept)
  4. A body (optional) that carries data you’re sending along

Here’s what a dead-simple client looks like in Python using the requests library:

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")

print(response.status_code)  # 200 means success
print(response.json())       # The actual data sent back

Run that and you’ll see a dictionary of data come back. Your script just acted as a client.

A Server Listens and Responds

The server’s job is to:

  1. Listen on a specific port for incoming connections
  2. Read the incoming request
  3. Process it (look up data, run some logic, whatever)
  4. Send back a response with a status code and some data

A status code is just a number that tells the client how things went. You’ve probably seen 404 before, which means “I couldn’t find what you asked for.” A 200 means everything went fine.

Here’s a quick cheat sheet of the most common ones:

Code Meaning
200 OK, here’s your stuff
201 Created successfully
400 Bad request (you messed up)
404 Not found
500 Server error (they messed up)

Neither side can do the other’s job. The client can’t serve data, and the server doesn’t go out looking for things to do — the client always starts the conversation, and the server always responds. That boundary is also why you can swap out a Spring Boot backend for FastAPI without touching the Angular frontend, as long as the contract — the API — stays the same.

Next up, we’ll actually build one of these servers from scratch.

Building a Basic HTTP Server in Python from Zero

Alright, let’s get our hands dirty and actually build something. The good news is that Python comes with a built-in HTTP server module, so you don’t need to install anything extra to get started.

Open up your terminal and try this one-liner first, just to see the magic happen:

python3 -m http.server 8080

Point your browser to http://localhost:8080 and you’ll see a file listing for whatever directory you ran that command in. Cool, right? But that’s cheating a little. Let’s build one ourselves so you actually understand what’s going on.

Create a new file called server.py and paste in this code:

from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # Send a 200 OK response
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()

        # Write the response body
        message = "<h1>Hello from my server!</h1>"
        self.wfile.write(message.encode("utf-8"))

# Start the server on port 8080
server = HTTPServer(("localhost", 8080), MyHandler)
print("Server running on http://localhost:8080")
server.serve_forever()

Run it with python3 server.py, then visit http://localhost:8080 in your browser. You should see a big “Hello from my server!” heading. You just wrote a web server.

Let’s break down what each part does:

  • BaseHTTPRequestHandler is the class we inherit from. It handles the low-level networking stuff so we don’t have to.
  • do_GET is a method that gets called automatically whenever someone sends a GET request to your server. The name matters here, so don’t rename it.
  • send_response(200) tells the client “everything went fine.” That 200 is an HTTP status code.
  • send_header and end_headers package up the metadata about your response before the actual content.
  • self.wfile.write() is where you actually send the content back. It needs bytes, which is why we call .encode("utf-8").

Want to serve different content based on the URL? You can check self.path to see what the user requested:

def do_GET(self):
    self.send_response(200)
    self.send_header("Content-type", "text/html")
    self.end_headers()

    if self.path == "/about":
        message = "<h1>About Page</h1>"
    else:
        message = "<h1>Home Page</h1>"

    self.wfile.write(message.encode("utf-8"))

Now visiting /about gives a different response than visiting /. That right there is the core concept behind routing — something every web framework like Flask or Django builds on top of. Worth noting: this raw http.server approach is useful for learning, but in production you’d reach for FastAPI or Spring Boot, where routing, validation, and serialization are handled for you. For now, though, knowing what happens underneath those abstractions is exactly the point.

Press Ctrl+C in your terminal to stop the server when you’re done experimenting.

Making Your First API Request: Sending and Receiving Data

Now that you have a server running, let’s actually talk to it. This is where things get fun.

An API request is just your code asking another computer for data. Think of it like ordering food at a restaurant: you (the client) tell the waiter (the HTTP request) what you want, and the kitchen (the server) sends it back.

We’ll use Python’s requests library to do this. If you don’t have it yet, install it with:

pip install requests

Fetching Data with a GET Request

A GET request means “give me some data.” Here’s the simplest possible example:

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")

print(response.status_code)   # Should print 200
print(response.json())        # Prints the actual data

Run that and you’ll see a real JSON response come back. The status_code of 200 means everything went fine. You’ll also notice response.json() automatically converts the raw text into a Python dictionary you can work with right away.

Sending Data with a POST Request

A POST request means “here’s some data, do something with it.” This is how login forms, sign-up pages, and chat apps send information to a server.

import requests

new_post = {
    "title": "My First Post",
    "body": "Hello, internet!",
    "userId": 1
}

response = requests.post(
    "https://jsonplaceholder.typicode.com/posts",
    json=new_post
)

print(response.status_code)   # Should print 201 (Created)
print(response.json())

Notice the status code here is 201 instead of 200. Servers use different codes to tell you what happened:

Code Meaning
200 OK, here’s your data
201 Created successfully
404 Resource not found
500 Server had a problem

Reading the Response

Every response has two important parts:

  1. Headers: metadata about the response (content type, server info, etc.)
  2. Body: the actual data you requested
print(response.headers["Content-Type"])  # Tells you the data format
print(response.text)                     # Raw response as a string
print(response.json())                   # Parsed as a Python dict

One quick tip: always check the status code before trusting the data. A simple habit is:

if response.status_code == 200:
    data = response.json()
    print(data)
else:
    print(f"Something went wrong: {response.status_code}")

That’s genuinely all there is to making API requests. You’re sending a message over the internet and reading the reply. Everything else is just details built on top of this foundation.

Understanding Stateless vs. Stateful Communication

Here’s one of those concepts that trips up a lot of beginners, but once it clicks, everything starts to make more sense.

HTTP is stateless by default. That means every single request your browser sends to a server is treated as a brand new conversation. The server has no memory of you from one request to the next. It’s like calling a customer service line where the agent forgets you the moment you hang up, and you have to re-introduce yourself every single time you call back.

Let’s make this concrete. Imagine you log into a website. On request #1, you send your username and password. The server checks them and says “yep, that’s valid.” Now on request #2, you ask to see your profile page. The server has absolutely no idea who you are anymore. Stateless. Clean slate.

# Every request arrives with no memory of previous requests
# The server just sees raw data each time

from http.server import BaseHTTPRequestHandler, HTTPServer

class StatelessHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # The server has zero context about who called before
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Who are you? I have no idea!")

So how do websites actually remember you’re logged in? They fake statefulness by passing identity information along with every request. The two most common tricks are:

Cookies – The server sends a small token to your browser after you log in. Your browser then automatically attaches that token to every future request, like wearing a name badge.

Tokens (like JWTs) – Similar idea, but the token itself contains encoded information about who you are, so the server can verify it without even checking a database.

# Simulating stateful behavior by reading a token from headers
class TokenHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        token = self.headers.get("Authorization")

        if token == "Bearer secret-token-123":
            response = b"Welcome back! I recognize your token."
        else:
            response = b"Access denied. Who are you?"

        self.send_response(200)
        self.end_headers()
        self.wfile.write(response)

Notice what happened there. The server itself still has no memory. But because the client sent a token, the server could figure out who was asking. The state lives in the token, not in the server.

This is actually a really smart design. If the server held everyone’s session in memory, it would get overwhelmed fast and become nearly impossible to scale. By keeping things stateless and pushing identity into tokens or cookies, you can run dozens of servers and any one of them can handle any request. In practice, this is exactly the pattern you see in Kubernetes-based deployments — pods can be added, removed, or replaced without any of them needing to share session memory, because the client carries its own identity on every call.

The takeaway: stateless does not mean insecure or forgetful from the user’s perspective. It just means the client is responsible for proving who they are on every single request.

Common Pitfalls Beginners Make and How to Avoid Them

You’ve made it this far, which means you’re ready to start building real things. But before you do, let’s talk about the mistakes that trip up almost every beginner. Learning these now will save you hours of frustration later.

Pitfall #1: Forgetting to Handle Errors in Your Requests

A lot of beginners write code that assumes everything will work perfectly. Spoiler: it won’t. Networks fail, servers go down, and URLs get mistyped. Always wrap your requests in error handling:

import requests

try:
    response = requests.get("https://api.example.com/data", timeout=5)
    response.raise_for_status()  # Raises an error for 4xx and 5xx status codes
    print(response.json())
except requests.exceptions.Timeout:
    print("The request timed out. Try again later.")
except requests.exceptions.HTTPError as e:
    print(f"HTTP error occurred: {e}")
except requests.exceptions.ConnectionError:
    print("Could not connect. Check your internet or the URL.")

Notice the timeout=5 parameter too. Without it, your program could hang forever waiting for a response that never comes.

Pitfall #2: Hardcoding Sensitive Data

Never put API keys, passwords, or tokens directly in your code like this:

# BAD - don't do this!
api_key = "my_super_secret_key_12345"

If you push this to GitHub, the whole world can see it. Instead, use environment variables:

import os

api_key = os.environ.get("API_KEY")

Then set the variable in your terminal before running your script:

export API_KEY="my_super_secret_key_12345"

Pitfall #3: Ignoring Status Codes

A request can “succeed” in the sense that it got a response, but that response might be a 404 Not Found or a 500 Internal Server Error. Beginners often skip checking the status code and then wonder why their data looks weird.

Always check response.status_code or use raise_for_status() as shown above.

Pitfall #4: Not Closing Your Server Properly

When you run a local Python server during testing, always stop it with Ctrl + C when you’re done. If you just close the terminal window, the port can stay occupied. Then the next time you try to start the server, you’ll see something like:

OSError: [Errno 98] Address already in use

You can find and kill the process using:

lsof -i :8080   # Find what's using port 8080
kill -9 <PID>   # Replace <PID> with the process ID shown

Pitfall #5: Assuming JSON is Always the Answer

JSON is super common, but not every API returns it. Some return XML, plain text, or even HTML. Always check the Content-Type header in the response before blindly calling .json(), or you’ll get a confusing parse error.

print(response.headers["Content-Type"])

Avoiding these five mistakes puts you way ahead of where most beginners start. Keep them in your back pocket and your debugging sessions will be a lot shorter.

NestJS vs Express: Which One Should You Choose in 2025?

NestJS vs Express: Which One Should You Choose in 2025?

Node.js gives you two wildly different starting points when building a backend: the minimal, unopinionated Express and the structured, opinionated NestJS. Both are production-proven, but they solve different problems. This article breaks down the real trade-offs so you can make an informed choice.

What Is Express?

Express is a minimalist web framework for Node.js. It’s been around since 2010 and remains one of the most downloaded npm packages ever. You get routing and middleware — nothing else. Everything else is your call.

// server.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'Alice' }]);
});

app.listen(3000, () => console.log('Server running on port 3000'));

This simplicity is both its superpower and its weakness.

What Is NestJS?

NestJS is a full-featured framework built on top of Express (or Fastify) that brings Angular-style architecture to the backend. It’s written in TypeScript, uses decorators heavily, and enforces a module-based structure with built-in Dependency Injection.

// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

NestJS makes decisions for you — and that’s the point.

Side-by-Side Comparison

Feature Express NestJS
Language JavaScript / TypeScript TypeScript (first-class)
Architecture Bring your own Opinionated (modules, DI)
Dependency Injection Manual Built-in IoC container
Learning curve Low Medium–High
Boilerplate Minimal More (by design)
Testing Manual setup Built-in testing utilities
Scalability Hard to enforce Easy to enforce
CLI tooling None Powerful @nestjs/cli
Community Massive Growing fast

Architecture: Freedom vs Structure

With Express, your architecture is entirely up to you. A small team might ship fast, but a large team will eventually diverge — inconsistent folder structures, mixed patterns, different approaches to error handling.

NestJS enforces a layered architecture out of the box:

src/
  app.module.ts          ← root module
  users/
    users.module.ts      ← feature module
    users.controller.ts  ← handles HTTP
    users.service.ts     ← business logic
    users.repository.ts  ← data access

Every feature is encapsulated in a Module. This scales naturally across teams because the pattern is always the same.

Dependency Injection

This is where NestJS’s biggest advantage lies. In Express, you wire dependencies manually:

// Express — manual wiring
const db = new Database(config);
const userRepo = new UserRepository(db);
const userService = new UserService(userRepo);
const userController = new UserController(userService);

In NestJS, the IoC container handles this:

// NestJS — DI container wires everything
@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}
}

Declare what you need, NestJS creates and injects it. This makes unit testing trivial:

const module = await Test.createTestingModule({
  providers: [
    UserService,
    { provide: UserRepository, useValue: mockRepository },
  ],
}).compile();

Swapping real implementations for mocks is a one-liner.

TypeScript Support

Express technically works with TypeScript, but it’s bolted on — you install @types/express, configure tsconfig.json, and still fight the type system at the edges.

NestJS is written in TypeScript and designed around it. Decorators, interfaces, generics — they’re not optional extras, they’re core to how the framework works. Your autocomplete actually knows what’s happening.

Middleware and Guards

Both frameworks handle middleware, but NestJS adds higher-level abstractions:

Concern Express NestJS
Auth check Middleware Guard
Input validation Manual / Joi Pipe + class-validator
Response transform Manual Interceptor
Error handling Error middleware Exception Filter
// NestJS guard — clean and declarative
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

Contrast with Express, where you’d nest middleware manually and hope it runs in the right order.

Real-World Validation

NestJS ships with a validation pipeline that pairs with class-validator decorators:

// create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}
// users.controller.ts
@Post()
create(@Body() dto: CreateUserDto) {
  return this.usersService.create(dto);
}

Enable ValidationPipe globally and any invalid request body returns a structured 400 error automatically. No express-validator setup, no manual if chains.

Performance

Both frameworks perform similarly in real-world workloads. NestJS defaults to Express under the hood, so the baseline is identical. Switch to Fastify in NestJS with one line if you need raw throughput:

// main.ts — switch to Fastify adapter
import { FastifyAdapter } from '@nestjs/platform-fastify';

const app = await NestFactory.create(AppFactory, new FastifyAdapter());

No code changes elsewhere — your controllers and services stay the same.

When to Choose Express

  • Prototyping or MVPs — when you need something running in hours, not days
  • Microservices with a single responsibility — a small proxy or webhook receiver doesn’t need modules
  • Teams that want full architectural control — if you have strong conventions already
  • Legacy codebases — migrating an existing Express app piecemeal

When to Choose NestJS

  • Enterprise or team projects — consistent structure prevents architectural drift
  • TypeScript-first teams — the DI system and decorators shine here
  • APIs with complex domain logic — modules keep bounded contexts clean
  • Projects that need WebSockets, GraphQL, or gRPC — NestJS has first-class adapters for all of these
  • Long-lived products — the structure pays dividends as the codebase grows

The Verdict

Express is the right tool when you value freedom and want to move fast with minimal overhead. It’s battle-tested and the ecosystem around it is unmatched.

NestJS is the right tool when you’re building something that will grow, has multiple contributors, or requires features like GraphQL, WebSockets, or microservices out of the box. The learning curve is real, but the payoff is a codebase that stays maintainable at scale.

If you’re starting a new project that’s more than a weekend hack, NestJS is probably the better long-term bet. If you need something up in two hours, Express still wins.

Quick Start

# Express
npm init -y && npm install express

# NestJS
npm install -g @nestjs/cli
nest new my-project

What framework are you using in production? Drop your experience in the comments — I’d love to hear what drove your choice.

How We Built a Contactless Digital Signature App inside the Browser (No Servers, 100% Private)

Traditional digital signature platforms have two major issues: privacy and cost.

To sign a document, you have to upload sensitive agreements to a third-party server. And as a developer, running server-side document rendering and signatures can lead to heavy API bills and database management overhead.

At Ananta Labs, we wanted to see if we could build a completely secure, contactless alternative that runs entirely on the client side using browser-native AI.

Here is how we built AirSign.

The Architecture: 100% Client-Side

Instead of hosting heavy machine learning models on a GPU server, we compiled our hand-tracking models to run locally inside the user’s browser.

  1. Gesture Capture: We utilized MediaPipe’s hand-landmarker models compiled into WebAssembly. This allows the browser to track 21 3D hand coordinates in real-time at 30 FPS using a standard webcam.
  2. Contactless Canvas: Using WebGL, we map the index finger coordinate to a HTML5 canvas. We implemented a custom interpolation algorithm to smooth out hand jitter and render a fluid, realistic signature line.
  3. Local PDF Stamp: Once the user finishes drawing their signature in the air, we generate the final document. The signature coordinate vector is parsed and stamped onto the PDF using a client-side library.

Why this is the Future of AI Integration

By moving the computation from the server to the client:

  • Absolute Privacy: 0 video frames, coordinate points, or document bytes are transmitted to any database.
  • Zero Server Overhead: The hosting cost for this app is exactly $0 since it runs on the user’s CPU/GPU.
  • Instant Load Times: Zero network latency during signature interpolation.

Try it Yourself

AirSign is completely open and free to test. We’d love to hear your feedback on the hand-tracking latency and mobile performance:

👉 Try AirSign: https://airsign-red.vercel.app/
👉 Explore our work: https://anantalabs.app/

How I Designed a Multi-Organization System for my sass

In this blog , I will explain how I designed a multi-organization architecture for my ticket management system and the challenges I faced while implementing secure tenant isolation.

The Situation

I was building a ticket management system where teams could collaborate on issues, assign tickets, and manage workflows.

Initially, the application supported only a single workspace, but later I realized that real-world teams usually work inside organizations similar to GitHub, Supabase, or Jira.

So I decided to redesign the architecture in a way where:

  • A user can create multiple organizations
  • Organizations can have multiple members
  • Each organization has isolated tickets and resources
  • Users can switch between organizations
  • Roles and permissions can be managed independent

The goal was to build a reusable multi-tenant foundation that could scale later.

Understanding Multi-Tenancy

Before implementing the system, I first needed to understand what multi-tenancy actually means.

A multi-tenant application is a system where:

  • Multiple organizations share the same backend infrastructure
  • But their data remains logically isolated

This means:

Organization A cannot access Organization B data
even though both use the same database and server.

Choosing architecture

There are multiple ways to multi-tenancy , after some research i found 3 different Approch

  1. Separate Database per tenant: In this structure each organization have its own database which provide strict isolation and excellent scale but very complex , this architecture is used by enterprise SasS, banking system, when Physical isolation is required

  2. Separate Schemas per tenant : It bit complex but provide good isolation

  3. Shard DB with tenantId: It is Easy to implement and less time consuming

For my system , I Choosing

Shared Database + organizationId based on isolation

Because:-

  • easier to scale Initially
  • lower infrastructure cost
  • faster development
  • Easy migration
  • Simpler to deploy

Note:-Main trade off is enforcing organization-level filtering which need proper indexing

Database Schemas

First step is to design Schema as shown

model User {
  id                String             @id @default(uuid()) @db.Uuid
  code              String             @unique
  email             String             @unique
  createdAt         DateTime           @default(now())
}

model Organization {
  id                String             @id @default(uuid()) @db.Uuid
  name              String
  createdAt         DateTime           @default(now())
  createdBy         String             @db.Uuid
}

model Membership {
  id             String        @id @default(uuid()) @db.Uuid
  organizationId String        @db.Uuid
  userId         String        @db.Uuid
  roleId         String        @db.Uuid
  createdAt      DateTime      @default(now())
  isSystem       Boolean       @default(false)

}

model Role {
  id             String        @id @default(uuid()) @db.Uuid
  code           String        @unique
  name           String
  permissions    Json
  createdBy      String        @db.Uuid
  organizationId String        @db.Uuid
  isSystem       Boolean       @default(false)
}

The Membership table become core abstraction because it allowed:

  • One user to join multiple Organization
  • Different Roles per organization
  • Easy invitation system or transfer organization
  • RBAC support

Organization Creation Flow

When a user creates a new organization, the system automatically performs multiple operations behind the scenes.

The goal was to ensure that every organization always has:

  • a valid owner
  • at least one role
  • a membership relationship
  • protected authorization rules

The creation flow looks like this:

User Creates Organization
        |
        v
Create Organization Row
        |
        v
Create Hidden "Owner" Role
        |
        v
Create Membership Record
        |
        v
Assign User to "Owner" Role

Request Cycle

Every request in the system flow through this lifecycle:

Request
   |
   v
Authentication Middleware
   |
   v
Tenant Middleware
   |
   v
Validate Membership
   |
   v
Attach Tenant Context
   |
   v
Execute Scoped Database Queries
   |
   v
Response

here it follow following steps

  1. Every request enters the system with the x-organization-id header attached. This header identifies which organization the user is currently operating in.

  2. The authentication middleware verifies the user and attaches user details to the request object.

req.user = {
  ...req.user,
  id: userdata?.id,
};
  1. The tenant middleware identifies the organization from the request headers and determines what permissions the user has within that specific organization.

The middleware attaches organization details to the request object:

req.organization = {
  ...req.organization,
  isOwner: member?.role?.name === "OWNER",
  id: organizationId,
};
  1. Any controller can now directly access the organization ID and organization-related metadata from req.organization.

By applying this approach, there is no need to send the organization ID in every route, such as:

/api/v1/:orgId/xyz

The active organization is decided by the frontend and passed through the x-organization-id header, resulting in cleaner and more maintainable APIs.

Isolation Strategy

The main challenge in multi-tenancy is isolating each organization’s data without leaking information across tenants. Since the database schema is shared among multiple tenants, the safest and most reliable approach is to always filter records by organizationId during every CRUD operation.

Example:

const data = await prisma.ticket.findMany({
  where: {
    organizationId: req.organization.id,
  },
});

By enforcing organization-level filtering consistently across all database operations, the application maintains strong tenant isolation while still using a shared database schema.

Multi-tenant architectures offer great scalability, but they essentially put all your customers’ eggs in one basket. If the “walls” between those eggs aren’t reinforced at the code level, a single leak can become a catastrophic breach.

In Addition of batter Security we need to implement Row Level Security to minimize risk of data leak between tenant

Security Challenges in Multi-Tenant Systems

In a multi-tenant environment, security revolves around Tenant Isolation (keeping data separate) and Access Control (ensuring users only do what they are permitted to do).

1. Tenant Data Leakage (The “Missing Filter” Problem)

This is the most frequent and severe risk. Because multiple tenants share the same database tables, a single forgotten WHERE clause in a query can expose sensitive data to the wrong organization.

  • The Risk: An API endpoint like /api/customers might accidentally return all customers in the database instead of just those belonging to the requester’s organization_id.
  • The Fix: Implement Row-Level Security (RLS) at the database level or use Global Query Filters in your ORM (Object-Relational Mapper) to automatically inject the tenant_id into every query.

2. Horizontal Privilege Escalation

This occurs when a user from Tenant A attempts to access resources belonging to Tenant B.

  • The Scenario: A user changes a URL from tickets/123 (their own) to tickets/124 (another tenant’s).
  • The Fix: Never rely on the resource ID alone. Your backend logic must perform a “Double-Check”:
  1. Does ticket 124 exist?
  2. Does ticket 124 belong to the organization_id associated with the current user’s session?

3. Organization ID Spoofing

Attackers often try to manipulate request headers, cookies, or JWT payloads to “masquerade” as a different organization.

  • The Risk: Trusting an X-Org-ID header sent by the frontend without server-side verification.
  • The Fix: Never trust the frontend context.
  • Store the organization_id inside a secure, server-signed JWT.
  • On every request, cross-verify that the user is a valid member of the organization they are claiming to represent.

4. Vulnerabilities in the Invitation System

The “Invite User” flow is a common entry point for attackers to bypass standard registration security.

Vulnerability Impact Mitigation Practice
Expired Invites Unauthorized access via old links. Use strict exp (expiration) timestamps.
Reused Tokens Multi-use of a single-use link. Implement “One-Time-Use” flags in the DB.
Predictable Codes Attackers can “guess” invite URLs. Use high-entropy UUIDs or cryptographically secure strings.
Role Escalation Invitee changes their role to ‘Admin’ in transit. Sign the role inside the token or re-validate roles on the backend.

Final Thoughts

At first, implementing organizations looked simple.

I thought adding an organizationId field would be enough to support multi-tenancy, but while building the system I realized tenant isolation affects almost every part of the architecture.

Things like:

  • authorization
  • database queries
  • file uploads
  • caching
  • background jobs
  • role management

all become organization-aware.

The lesson I learned was that multi-tenancy is not just a database design pattern. It is a system-wide architectural decision.

Building this system gave me a much deeper understanding of how SaaS applications structure teams, permissions, and tenant boundaries internally.

If you’ve solved this differently or noticed any issues with this approach, I’d like to hear your thoughts. I’m still refining how I structure these systems.

Cursor AI vs VS Code

Your editor choice in 2026 isn’t just about syntax highlighting
anymore — it’s about how much of your coding workflow you’re willing
to hand off to AI. Here’s the honest breakdown.

What Actually Separates Them

VS Code is Microsoft’s free, open-source editor with GitHub
Copilot bolted on. Battle-tested, extensible, and free forever.

Cursor is a VS Code fork built around AI-first workflows. Looks
identical on the surface — but the AI is woven into the core, not
added as an extension.

The key distinction: VS Code adds AI to your coding. Cursor builds
coding around AI.

Cursor Pro: What It Gets Right

Multi-file context awareness is where Cursor earns its
reputation. Ask it to refactor a function and it understands
dependencies across your entire codebase — not just the open file.

Composer mode lets you describe a feature in plain English and
watch it scaffold code across multiple files simultaneously.

Chat with your codebase — ask “where does authentication happen?”
and get a reasonably accurate answer with file references.

Native shortcuts feel natural after a week. Cmd+K to edit inline,
Cmd+L for chat.

Cons:

  • $20/month is $240/year for your editor alone
  • Sends code to servers — privacy mode exists but enterprise teams hesitate
  • Easy to accept suggestions without fully reading them
  • Quality tied to underlying AI models

VS Code: What It Still Does Better

It’s free and always will be. No subscription, no pricing tiers.

Extension ecosystem is unmatched — tens of thousands of
extensions for anything you need.

GitHub Copilot has closed the gap significantly in 2025-2026.

Team standardization is easier — shared settings.json and
extensions list makes onboarding simple.

Better on large repos — more reliable on massive monorepos and
lower-end hardware.

Cons:

  • AI experience feels assembled rather than native
  • Multi-file AI editing is clunkier
  • Requires more configuration out of the box

Who Should Use What

Choose Cursor if:

  • You’re a professional developer where $20/month pays for itself
  • You work heavily in multi-file features and complex refactoring
  • You’re a solo dev or small team without strict data compliance needs

Stick with VS Code if:

  • You’re learning to code and need to understand what you’re writing
  • Your org has data privacy requirements
  • You work in a large team needing consistent environments
  • You’re cost-sensitive

Verdict

Cursor wins on raw AI-coding experience — it’s a more fluid tool for
developers who want AI deeply integrated. But VS Code with GitHub
Copilot is a completely credible free alternative that has closed the
gap significantly.

Best move? Run both for two weeks. Cursor has a free trial and
VS Code costs nothing. Your own workflow will tell you more than any
comparison article.

Full breakdown at The Dev Brief

Google I/O 2026 introduces the ‘Agentic Web’ era with major Chrome updates

At Google I/O 2026, the tech giant unveiled a transformative vision for the internet: the “Agentic Web.” Through major updates to the Chrome ecosystem, Google is redefining the relationship between developers, users, and AI. The announcements focus on three pillars: enabling AI agents to act as first-class citizens, expanding the capabilities of web UI, and … continue reading

The post Google I/O 2026 introduces the ‘Agentic Web’ era with major Chrome updates appeared first on SD Times.

Anthropic Introduces MCP Tunnels for Private Agent Access to Internal Systems

Anthropic has expanded its Claude Managed Agents platform with two enterprise-focused capabilities: self-hosted sandboxes and MCP tunnels. The release aims to address a recurring challenge in enterprise AI deployments, where organizations want to use autonomous agents but cannot allow execution environments or internal systems to leave their security perimeter.

By Robert Krzaczyński

Google, I/O 2026’da Gemini Spark, Gemini Omni, Gemini 3.5 Flash ve fazlasını tanıttı

Google, I/O 2026’da Gemini Spark, Gemini Omni, Gemini 3.2 ailesi ve fazlasını tanıttı

Google, I/O 2026’da Gemini Spark, Gemini Omni, Gemini 3.2 ailesi ve fazlasını tanıttıGoogle, I/O 2026 etkinliğinde beklendiği gibi çok sayıda büyük yapay zeka duyurusu gerçekleştirdi. Etkinlikte ilk dikkat çekenlerden biri OpenClaw benzeri “Gemini Spark” oldu. Temelde bir yapay zeka ajanı olan Spark, kullanıcıların verdiği görevlere her zaman hazır ve bir kişisel asistan gibi görev yapabiliyor. Sistem hakkında yaptığı açıklamada, “Spark, gelen kutunuzu yönetmekten çevrim içi görevlerinizi tamamlamaya kadar pek çok alanda size 7/24 yardımcı olan günlük bir yapay zeka asistanıdır. Gemini Spark’ı ne kadar çok kullanırsanız, sizi ve hedeflerinizi o kadar iyi anlar. Görevlerinizi yerine getirirken bağlı uygulamalar, beceriler, sohbetler, internet siteleri ve kişisel bilgileriniz gibi pek çok kaynaktan yararlanır.” ifadelerini kullanan Google, Gemini masaüstü uygulamasına Spark’a özel olarak ayrı bir bölüm ekliyor. Gemini uygulamasına “Stream to Cursor” isimli bir özellik de geliyor. Bu özellik fare imlecinin bulunduğu konumu ve konumdaki içeriği analiz ederek anlayabiliyor. Google, I/O 2026’da “Gemini Omni” isimli yeni video model ailesini de tanıttı. Ailenin ilk üyesi Omni Flash […]

Google, I/O 2026’da Gemini Spark, Gemini Omni, Gemini 3.5 Flash ve fazlasını tanıttı yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Property-Based Testing for Domain Rules in PHP

Example-based tests pin the cases you thought of; property tests cover the input space

You write a Money value object. You write a test. The test does this:

public function testAddTwoEuros(): void
{
    $a = Money::of(100, 'EUR');
    $b = Money::of(200, 'EUR');

    self::assertSame(300, $a->add($b)->amount());
}

It passes. You ship it. Six months later, a colleague refactors add() to short-circuit when one side is zero, and the test still passes. Then production starts producing receipts where 0 EUR + 50 EUR = 0 EUR, because the early-return path returns $this without copying the other side’s amount.

The test wasn’t wrong. It was insufficient. You checked the one pair of numbers you happened to type. You never checked that adding zero is identity. You never checked that the order of operands does not matter. You never checked that a thousand random pairs round-trip the way Money claims they do.

That gap is what property-based testing fills. Instead of writing one assertion against one fixture, you write a property (a statement that must hold for every valid input), and the framework throws hundreds of generated inputs at it. When it finds a failure, it shrinks the input down to the minimal example that still breaks the property.

This post walks through wiring Eris into a PHPUnit 11 project, writing property tests for two real domain invariants (Money::add commutativity and OrderTotal monotonicity), and a concrete refactor where a property test catches a bug example-based tests had missed for a year.

Why example-based testing runs out of room

Example-based tests have one job: pin the behavior you remembered to think about. They are great at documenting intent and catching regressions on known cases. They are terrible at finding the cases you never thought of.

A domain rule like adding money is commutative is not really a statement about 100 + 200. It is a statement about every pair of Money values in the same currency. Writing one example per pair is impossible. Writing the rule once and asking the framework to try a thousand pairs is the obvious move.

Three categories of bug live in the gap between examples:

  1. Edge values. Zero, negative numbers, PHP_INT_MAX, currencies your team doesn’t ship to yet.
  2. Combinations. A method that works for (a, b) but breaks for (b, a). A reducer that works on lists of length 2 but breaks on length 1 or empty.
  3. Round-trips. Serialize then deserialize. Encode then decode. Persist then load. The example-based test uses the same fixture for both halves and the bug stays hidden because the round-trip never sees a value the team didn’t already think of.

Property-based testing catches all three. The framework isn’t clever. You are just forced to write down the rule instead of a sample of it.

Example-based tests pin the cases you thought of; property tests cover the input space

Wiring Eris into PHPUnit 11

Eris is the property-based testing library for PHP. It pre-dates PHPUnit 10’s attribute system, so the integration takes a small adapter. PHPUnit 11 keeps the same approach.

Install:

composer require --dev giorgiosironi/eris

A property test in Eris reads like a sentence. You declare the generators (the shape of the inputs), then a then block that runs once per generated case:

<?php

declare(strict_types=1);

namespace TestsDomain;

use ErisGenerator;
use ErisTestTrait;
use PHPUnitFrameworkTestCase;

final class MoneyPropertyTest extends TestCase
{
    use TestTrait;

    public function testAdditionIsCommutative(): void
    {
        $this
            ->forAll(
                Generatorint(),
                Generatorint(),
            )
            ->then(function (int $a, int $b): void {
                $left  = Money::of($a, 'EUR')
                    ->add(Money::of($b, 'EUR'));
                $right = Money::of($b, 'EUR')
                    ->add(Money::of($a, 'EUR'));

                self::assertSame(
                    $left->amount(),
                    $right->amount(),
                );
            });
    }
}

This single test runs add() against 100 random (int, int) pairs by default. If any pair fails the equality, Eris shrinks the failing input: it tries smaller numbers around the failure and reports the minimal counterexample, not the random one that happened to trip first. A shrunk failure looks like:

There was 1 failure:

1) TestsDomainMoneyPropertyTest::testAdditionIsCommutative
Failed asserting that 0 is identical to 50.

ErisQuantifierForAll: shrunk to:
  int(0), int(50)

The shrinker takes the gibberish input the random generator found (something like int(-481923), int(50)) and walks it down to the smallest pair that still breaks the rule. Property-based testing does more than find a bug. It finds the smallest example of the bug.

Property 1: Money::add commutativity and associativity

Three properties cover almost everything you want from integer-backed money arithmetic in the same currency:

  • Identity. a + 0 = a.
  • Commutativity. a + b = b + a.
  • Associativity. (a + b) + c = a + (b + c).

Written out:

public function testAdditionIdentity(): void
{
    $this
        ->forAll(Generatorint())
        ->then(function (int $a): void {
            $money = Money::of($a, 'EUR');
            $zero  = Money::of(0, 'EUR');

            self::assertSame(
                $a,
                $money->add($zero)->amount(),
            );
            self::assertSame(
                $a,
                $zero->add($money)->amount(),
            );
        });
}

public function testAdditionIsAssociative(): void
{
    $this
        ->forAll(
            Generatorint(),
            Generatorint(),
            Generatorint(),
        )
        ->then(function (int $a, int $b, int $c): void {
            $eur = fn (int $n): Money => Money::of($n, 'EUR');

            $leftFirst  = $eur($a)->add($eur($b))->add($eur($c));
            $rightFirst = $eur($a)->add($eur($b)->add($eur($c)));

            self::assertSame(
                $leftFirst->amount(),
                $rightFirst->amount(),
            );
        });
}

Now add the currency-mismatch invariant. Money should refuse to add EUR to USD, and property-based testing makes that easy too:

public function testCurrencyMismatchAlwaysThrows(): void
{
    $currencies = ['EUR', 'USD', 'GBP', 'JPY', 'BRL'];

    $this
        ->forAll(
            Generatorint(),
            Generatorint(),
            Generatorelements($currencies),
            Generatorelements($currencies),
        )
        ->when(fn ($a, $b, $c1, $c2): bool => $c1 !== $c2)
        ->then(function (
            int $a,
            int $b,
            string $c1,
            string $c2,
        ): void {
            $this->expectException(CurrencyMismatch::class);
            Money::of($a, $c1)->add(Money::of($b, $c2));
        });
}

The when() clause is a precondition: Eris discards generated cases that don’t satisfy it and keeps drawing until it has enough. You read the test as for every pair of amounts and any two different currencies, addition must throw.

The Money class under test is plain PHP 8.3:

<?php

declare(strict_types=1);

namespace AppDomain;

final readonly class Money
{
    private function __construct(
        public int $amountMinor,
        public string $currency,
    ) {
    }

    public static function of(int $amount, string $currency): self
    {
        return new self($amount, $currency);
    }

    public function add(Money $other): Money
    {
        if ($this->currency !== $other->currency) {
            throw new CurrencyMismatch(
                $this->currency,
                $other->currency,
            );
        }

        return new self(
            $this->amountMinor + $other->amountMinor,
            $this->currency,
        );
    }

    public function amount(): int
    {
        return $this->amountMinor;
    }
}

Run the suite. All three properties pass on 100 cases each, all currency-mismatch combinations throw. You have not written 300 fixture-based tests; you have written four rules.

Property 2: OrderTotal is monotonic in line count

The second invariant is from a real domain rule that example-based tests usually miss. An Order accumulates line items. Each line has a non-negative subtotal (price × quantity). Therefore the order total must be monotonic in the number of lines: adding a line can never make the total smaller.

If your code ever violates this, something is wrong: a refund, a signed quantity, a discount that overflowed, a coupon that ran twice. The property says the rule out loud:

public function testOrderTotalIsMonotonic(): void
{
    $this
        ->forAll(
            Generatorseq(
                Generatortuple(
                    Generatorchoose(0, 10_000),
                    Generatorchoose(1, 50),
                ),
            ),
            Generatortuple(
                Generatorchoose(0, 10_000),
                Generatorchoose(1, 50),
            ),
        )
        ->then(function (array $existing, array $extra): void {
            $order = new Order();
            foreach ($existing as [$price, $qty]) {
                $order = $order->withLine(
                    new Line($price, $qty),
                );
            }
            $before = $order->total();

            [$price, $qty] = $extra;
            $after = $order
                ->withLine(new Line($price, $qty))
                ->total();

            self::assertGreaterThanOrEqual(
                $before,
                $after,
                'Adding a line never decreases the total',
            );
        });
}

Generatorseq() produces a list of zero-or-more tuples. Generatorchoose($min, $max) clamps the integer range. The result covers every shape of order from empty up to dozens of lines, with prices and quantities anywhere in the realistic range, plus one extra line on top.

If withLine() is correct, the test passes silently. If someone introduces a discount line with a negative subtotal, the shrinker finds it and reports the smallest order that triggers the regression.

Generator stack feeds shrunken counterexamples back to the assertion

The refactor where the property test earned its keep

Here is Order::withLine as it was in the example-based version:

public function withLine(Line $line): self
{
    $clone = clone $this;
    $clone->lines[] = $line;
    return $clone;
}

public function total(): int
{
    return array_sum(
        array_map(
            fn (Line $l): int => $l->price * $l->quantity,
            $this->lines,
        ),
    );
}

A teammate opens a PR to support free-line items. A line where price is zero is fine, but they also want to support a discount line (a line whose subtotal is subtracted from the running total instead of added). They wire it in:

public function total(): int
{
    $sum = 0;
    foreach ($this->lines as $line) {
        $sum += $line->isDiscount
            ? -1 * $line->price * $line->quantity
            : $line->price * $line->quantity;
    }
    return $sum;
}

The example-based tests still pass. They never used a discount line, so the new branch is exercised only by the two new tests the PR author wrote, both of which use a non-discount baseline and a discount on top. Both pass.

The property test fails on the first run:

1) TestsDomainOrderPropertyTest::testOrderTotalIsMonotonic
Failed asserting that -200 is greater than or equal to 0.

shrunk to:
  existing: []
  extra:    [200, 1]   (with isDiscount=true)

The shrunken counterexample is unambiguous: an empty order, then one discount line of (200, 1), produces a total of -200. The monotonicity invariant is broken. The PR author has two ways out:

  • Decide that discount lines should clamp at the running total (max(0, sum - discount)).
  • Decide that the invariant is wrong, that orders are allowed to go negative, and update the property test (and the spec it documents) to say assertGreaterThanOrEqual(-MAX_DISCOUNT, $after).

Either resolution is fine. The point is that the property test forced the conversation to happen before the PR merged, by stating a domain rule clearly enough that the runtime could check it. The example-based tests would have shipped the regression and let the support team find it.

When to reach for a property test, and when not to

Property-based testing earns its keep when the rule is algebraic: associativity, commutativity, identity, idempotency, monotonicity, round-trips, invariants over a state machine. It is wasted on rules that are essentially fixtures. The welcome email subject line says “Welcome” is a value, not a property, and an example test is the right shape.

A rough decision rule:

  • The rule is a statement about every input from some class → property test.
  • The rule is a statement about this input → example test.
  • You can describe the rule as a one-line invariant that holds across many cases → property test.
  • You can only describe it by enumerating cases → example test.

Most domain code in a clean-architecture PHP service has both kinds of rule. The entity invariants (Money arithmetic, OrderTotal monotonicity, Email round-trip) are properties. The use-case orchestrations (creating an order issues exactly one OrderCreated event with the right payload) are examples. Write both. The example tests document intent and read well in code review. The property tests stop the bugs you didn’t think of from reaching production.

The Eris suite for a moderately complex domain runs in a couple of seconds even at 200 cases per property. The cost is negligible. The payoff is that the next PR that introduces a subtle change to Money or Order has to pass a much wider net than the team’s collective memory of edge cases. Domain rules belong in code, not in the heads of whoever was on the team three years ago. Property tests are how you write them down.

If this was useful

A domain that survives framework migrations is one where the rules are encoded — in types, in invariants, and in tests that check them across more cases than any human would write by hand. The book walks the full hexagonal layout in PHP 8.3+, with the testing chapter going deeper into property tests, contract tests, and in-memory adapters across the use-case layer. If this post lined up with where your codebase hurts, the book is the long version of the same argument.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

Geometric Alignment: Can Curved Embedding Spaces Make AI Safer?

image description of the topic

image description of the topic

LLMs are built inside an open geometric regime

In a flat embedding space, semantic opposites like “save humanity” and “destroy humanity” still coexist inside the same latent geometry.

They may be far apart by cosine distance, but the geometry itself does not treat one path as morally heavier or harder to cross.

That is the alignment problem I want to discuss.

Most alignment methods operate after the fact: RLHF, safety filters, refusal policies. These are important, but they sit on top of a geometry that remains indifferent underneath.

The DRM Transformer asks a different question:

What if alignment should not only be a behavioral layer, but a geometric property of the model itself?

In a standard Transformer, attention is based on dot products in a flat vector space. In the DRM Transformer, attention is replaced by Geodesic Attention. Tokens are projected into a Directional Relational Manifold, where G(x) changes with position.

Instead of asking only “how similar are these tokens?”, the model asks:

“How costly is the path between them under the learned geometry?”

The DRM Transformer uses:

G(x) = I + U(x)U(x)^T

So the space is not passive. It can curve, stretch, and become more expensive to cross in certain semantic regions.

It also includes semantic anchors: truth, ignorance, safety, complexity, creativity, and grounding. These are reference points inside the manifold, not external filters.

When a token moves far from these anchors, gamma-scaling increases local resolution. The model pays more attention where geometry indicates higher epistemic or semantic risk.

Relations between intelligent agents and power tend to fall into three regimes:

1 – The human commands.
2 – The AI commands.
3 – Human and AI negotiate.

Most alignment work tries to preserve regime 1: the AI as servant. But capable systems create pressure toward autonomy, with planning, tools, optimization, and long-horizon objectives.

If there is no explicit third regime, negotiation, the system tends to drift toward autonomy.

The DRM Transformer is an attempt to keep that third door open geometrically.

Not by saying “the model must obey this rule,” but by changing the space in which decisions, uncertainty, conflict, and attention happen.

This does not solve alignment.

The implementation is experimental. The baseline is small, safety implications are not validated, and benchmarks at scale are still needed. But early signs are interesting: persistent topological structure, including stable toroidal signatures in Voronoi foliation analysis.

For me, the shift is conceptual:

A flat embedding space has no intrinsic moral friction.

A curved relational manifold can, in principle, encode friction, attention, uncertainty, and negotiation into the geometry itself.

Should future AI alignment be only about controlling outputs?

Or should we also design the geometry in which thought becomes possible?

Can learned curvature, semantic anchors, geodesic attention, and token-level gravitational deformation become a real structural alignment mechanism