Skip to content

Vercel BotID: The Invisible Bot Protection You Needed

Vercel BotID: The Invisible Bot Protection You Needed

Nowadays, bots do not act like “bots”. They can execute JavaScript, solve CAPTCHAs, and navigate as real users. Traditional defenses often fail to meet expectations or frustrate genuine users.

That’s why Vercel created BotID, an invisible CAPTCHA that has real-time protections against sophisticated bots that help you protect your critical endpoints.

In this blog post, we will explore why you should care about this new tool, how to set it up, its use cases, and some key considerations to take into account.

We will be using Next.js for our examples, but please note that this tool is not tied to this framework alone; the only requirement is that your app is deployed and running on Vercel.

Why Should You Care?

Think about these scenarios:

  • Checkout flows are overwhelmed by scalpers
  • Signup forms inundated with fake registrations
  • API endpoints draining resources with malicious requests

They all impact you and your users in a negative way. For example, when bots flood your checkout page, real customers are unable to complete their purchases, resulting in your business losing money and damaging customer trust.

Fake signups clutter the app, slowing things down and making user data unreliable. When someone deliberately overloads your app’s API, it can crash or become unusable, making users angry and creating a significant issue for you, the owner.

BotID automatically detects and filters bots attempting to perform any of the above actions without interfering with real users.

How does it work? A lightweight first-party script quickly gathers a high set of browser & environment signals (this takes ~30ms, really fast so no worry about performance issues), packages them into an opaque token, and sends that token with protected requests via the rewritten challenge/proxy path + header; Vercel’s edge scores it, attaches a verdict, and checkBotId() function simply reads that verdict so your code can allow or block. We will see how this is implemented in a second! But first, let’s get started.

Getting Started in Minutes

  1. Install the SDK:
npm install botid
  1. Configure redirects

Wrap your next.config.ts with BotID’s helper. This sets up the right rewrites so BotID can do its job (and not get blocked by ad blockers, extensions, etc.):

import { withBotId } from 'botid/next/config';

const nextConfig = {
  // Your existing Next.js config
};

export default withBotId(nextConfig);
  1. Integrate the client on public-facing pages (where BotID runs checks):

Declare which routes are protected so BotID can attach special headers when a real user triggers those routes.

We need to create instrumentation-client.ts (place it in the root of your application or inside a src folder) and initialize BotID once:

import { initBotId } from 'botid/client/core';

// Define the paths that need bot protection.
// These are paths that are routed to by your app.
// These can be:
// - API endpoints (e.g., '/api/checkout')
// - Server actions invoked from a page (e.g., '/dashboard')
// - Dynamic routes (e.g., '/api/create/*')

initBotId({
  protect: [
    {
      path: '/api/checkout',
      method: 'POST',
    },
    {
      // Wildcards can be used to expand multiple segments
      // /team/*/activate will match
      // /team/a/activate
      // /team/a/b/activate
      // /team/a/b/c/activate
      // ...
      path: '/team/*/activate',
      method: 'POST',
    },
    {
      // Wildcards can also be used at the end for dynamic routes
      path: '/api/user/*',
      method: 'POST',
    },
  ],
});

instrumentation-client.ts runs before the app hydrates, so it’s a perfect place for a global setup!

If we have an inferior Next.js version than 15.3, then we would need to use a different approach. We need to render the React component inside the pages or layouts you want to protect, specifying the protected routes:

import { BotIdClient } from 'botid/client';
import { ReactNode } from 'react';

const protectedRoutes = [
  {
    path: '/api/checkout',
    method: 'POST',
  },
];

type RootLayoutProps = {
  children: ReactNode;
};

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html lang="en">
      <head>
        <BotIdClient protect={protectedRoutes} />
      </head>
      <body>{children}</body>
    </html>
  );
}

  1. Verify requests on your server or API:
import { checkBotId } from 'botid/server';

export async function POST(req: Request) {
  const { isBot } = await checkBotId();

  if (isBot) {
   return new Response("Access Denied", { status: 403 });
  } 

  return new Response("✅ Success!");
}
  • NOTE: checkBotId() will fail if the route wasn’t listed on the client, because the client is what attaches the special headers that let the edge classify the request!

You’re all set - your routes are now protected!

In development, checkBotId() function will always return isBot = false so you can build without friction. To disable this, you can override the options for development:

const { isBot } = await checkBotId({
    developmentOptions: {
      bypass: 'BAD-BOT', // default: 'HUMAN'
    },
  });

What happens on a failed check?

In our example above, if the check failed, we return a 403, but it is mostly up to you what to do in this case; the most common approaches for this scenario are:

  • Hard block with a 403 for obviously automated traffic (just what we did in the example above)
  • Soft fail (generic error/“try again”) when you want to be cautious.
  • Step-up (require login, email verification, or other business logic).

Remember, although rare, false positives can occur, so it’s up to you to determine how you want to balance your fail strategy between security, UX, telemetry, and attacker behavior.

checkBotId()

So far, we have seen how to use the property isBot from checkBotId(), but there are a few more properties that you can leverage from it. There are:

isHuman (boolean): true when BotID classifies the request as a real human session (i.e., a clear “pass”). BotID is designed to return an unambiguous yes/no, so you can gate actions easily.

isBot (boolean): We already saw this one. It will be true when the request is classified as automated traffic.

isVerifiedBot (boolean): Here comes a less obvious property. Vercel maintains and continuously updates a comprehensive directory of known legitimate bots from across the internet. This directory is regularly updated to include new legitimate services as they emerge. This could be helpful for allowlists or custom logic per bot. We will see an example in a sec.

verifiedBotName? (string): The name for the specific verified bot (e.g., “claude-user”).

verifiedBotCategory? (string): The type of the verified bot (e.g., “webhook”, “advertising”, “ai_assistant”).

bypassed (boolean): it is true if the request skipped BotID check due to a configured Firewall bypass (custom or system). You could use this flag to avoid taking bot-based actions when you’ve explicitly bypassed protection.

Handling Verified Bots

  • NOTE: Handling verified bots is available in botid@1.5.0 and above.

It might be the case that you don’t want to block some verified bots because they are not causing damage to you or your users, as it can sometimes be the case for AI-related bots that fetch your site to give information to a user.

We can use the properties related to verified bots from checkBotId() to handle these scenarios:

import { checkBotId } from "botid/server";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const botResult = await checkBotId();

  const { isBot, verifiedBotName, isVerifiedBot } = botResult;

  // Check if it's ChatGPT Operator
  const isOperator = isVerifiedBot && verifiedBotName === "chatgpt-operator";

  if (isBot && !isOperator) {
    return Response.json({ error: "Access denied" }, { status: 403 });
  }

  // ... rest of your handler
  return Response.json(botResult);
} 

Choosing your BotID mode

When leveraging BotID, you can choose between 2 modes:

  • Basic Mode: Instant session-based protection, available for all Vercel plans.
  • Deep Analysis Mode: Enhanced Kasada-powered detection, only available for Pro and Enterprise plan users. Using this mode, you will leverage a more advanced detection and will block the hardest to catch bots

To specify the mode you want, you must do so in both the client and the server. This is important because if either of the two does not match, the verification will fail!

// Client side
initBotId({
  protect: [
    {
      path: '/api/checkout',
      method: 'POST',
      advancedOptions: {
        checkLevel: 'deepAnalysis'
      },
    },
 ...
  ],
});

// Server side
export async function POST(request: NextRequest) {
  const verification = await checkBotId({
    advancedOptions: {
      checkLevel: 'deepAnalysis', // Must match client-side config
    },
  });

  if (verification.isBot) {
    return NextResponse.json({ error: 'Access denied' }, { status: 403 });
  }

  // Your protected logic here
}

Conclusion

Stop chasing bots - let BotID handle them for you! Bots are and will get smarter and more sophisticated. BotID gives you a simple way to push back without slowing your customers down. It is simple to install, customize, and use.

Stronger protection equals fewer headaches. Add BotID, ship with confidence, and let the bots trample into a wall without knowing what’s going on.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Introduction to Vercel’s Flags SDK cover image

Introduction to Vercel’s Flags SDK

Introduction to Vercel’s Flags SDK In this blog, we will dig into Vercel’s Flags SDK. We'll explore how it works, highlight its key capabilities, and discuss best practices to get the most out of it. You'll also understand why you might prefer this tool over other feature flag solutions out there. And, despite its strong integration with Next.js, this SDK isn't limited to just one framework—it's fully compatible with React and SvelteKit. We'll use Next.js for examples, but feel free to follow along with the framework of your choice. Why should I use it? You might wonder, "Why should I care about yet another feature flag library?" Unlike some other solutions, Vercel's Flags SDK offers unique, practical features. It offers simplicity, flexibility, and smart patterns to help you manage feature flags quickly and efficiently. It’s simple Let's start with a basic example: ` This might look simple — and it is! — but it showcases some important features. Notice how easily we can define and call our flag without repeatedly passing context or configuration. Many other SDKs require passing the flag's name and context every single time you check a flag, like this: ` This can become tedious and error-prone, as you might accidentally use different contexts throughout your app. With the Flags SDK, you define everything once upfront, keeping things consistent across your entire application. By "context", I mean the data needed to evaluate the flag, like user details or environment settings. We'll get into more detail shortly. It’s flexible Vercel’s Flags SDK is also flexible. You can integrate it with other popular feature flag providers like LaunchDarkly or Statsig using built-in adapters. And if the provider you want to use isn’t supported yet, you can easily create your own custom adapter. While we'll use Next.js for demonstration, remember that the SDK works just as well with React or SvelteKit. Latency solutions Feature flags require definitions and context evaluations to determine their values — imagine checking conditions like, "Is the user ID equal to 12?" Typically, these evaluations involve fetching necessary information from a server, which can introduce latency. These evaluations happen through two primary functions: identify and decide. The identify function gathers the context needed for evaluation, and this context is then passed as an argument named entities to the decide function. Let's revisit our earlier example to see this clearly: ` You could add a custom evaluation context when reading a feature flag, but it’s not the best practice, and it’s not usually recommended. Using Edge Config When loading our flags, normally, these definitions and evaluation contexts get bootstrapped by making a network request and then opening a web socket listening to changes on the server. The problem is that if you do this in Serverless Functions with a short lifespan, you would need to bootstrap the definitions not just once but multiple times, which could cause latency issues. To handle latency efficiently, especially in short-lived Serverless Functions, you can use Edge Config. Edge Config stores flag definitions at the Edge, allowing super-fast retrieval via Edge Middleware or Serverless Functions, significantly reducing latency. Cookies For more complex contexts requiring network requests, avoid doing these requests directly in Edge Middleware or CDNs, as this can drastically increase latency. Edge Middleware and CDNs are fast because they avoid making network requests to the origin server. Depending on the end user’s location, accessing a distant origin can introduce significant latency. For example, a user in Tokyo might need to connect to a server in the US before the page can load. Instead, a good pattern that the Flags SDK offers us to avoid this is cookies. You could use cookies to store context data. The browser automatically sends cookies with each request in a standard format, providing consistent (no matter if you are in Edge Middleware, App Router or Page Router), low-latency access to evaluation context data: ` You can also encrypt or sign cookies for additional security from the client side. Dedupe Dedupe helps you cache function results to prevent redundant evaluations. If multiple flags rely on a common context method, like checking a user's region, Dedupe ensures the method executes only once per runtime, regardless of how many times it's invoked. Additionally, similar to cookies, the Flags SDK standardizes headers, allowing easy access to them. Let's illustrate this with the following example: ` Server-side patterns for static pages You can use feature flags on the client side, but that will lead to unnecessary loaders/skeletons or layout shifts, which are never that great. Of course, it brings benefits, like static rendering. To maintain static rendering benefits while using server-side flags, the SDK provides a method called precompute. Precompute Precompute lets you decide which page version to display based on feature flags and then we can cache that page to statically render it. You can precompute flag combinations in Middleware or Route Handlers: ` Next, inside a middleware (or route handler), we will precompute these flags and create static pages per each combination of them. ` The user will never notice this because, as we use “rewrite”, they will only see the original URL. Now, on our page, we “invoke” our flags, sending the code from the params: ` By sending our code, we are not really invoking the flag again but getting the value right away. Our middleware is deciding which variation of our pages to display to the user. Finally, after rendering our page, we can enable Incremental Static Regeneration (ISR). ISR allows us to cache the page and serve it statically for subsequent user requests: ` Using precompute is particularly beneficial when enabling ISR for pages that depend on flags whose values cannot be determined at build time. Headers, geo, etc., we can’t know their value at build, so we use precompute() so the Edge can evaluate it on the fly. In these cases, we rely on Middleware to dynamically determine the flag values, generate the HTML content once, and then cache it. At build time, we simply create an initial HTML shell. Generate Permutations If we prefer to generate static pages at build-time instead of runtime, we can use the generatePermutations function from the Flags SDK. This method enables us to pre-generate static pages with different combinations of flags at build time. It's especially useful when the flag values are known beforehand. For example, scenarios involving A/B testing and a marketing site with a single on/off banner flag are ideal use cases. ` ` Conclusion Vercel’s Flags SDK stands out as a powerful yet straightforward solution for managing feature flags efficiently. With its ease of use, remarkable flexibility, and effective patterns for reducing latency, this SDK streamlines the development process and enhances your app’s performance. Whether you're building a Next.js, React, or SvelteKit application, the Flags SDK provides intuitive tools that keep your application consistent, responsive, and maintainable. Give it a try, and see firsthand how it can simplify your feature management workflow!...

Internationalization in Next.js with next-intl cover image

Internationalization in Next.js with next-intl

Internationalization in Next.js with next-intl Internationalization (i18n) is essential for providing a multi-language experience for global applications. next-intl integrates well with Next.js’ App Router, handling i18n routing, locale detection, and dynamic configuration. This guide will walk you through setting up i18n in Next.js using next-intl for URL-based routing, user-specific settings, and domain-based locale routing. Getting Started First, create a Next.js app with the App Router and install next-intl: ` Next, configure next-intl in the next.config.ts file to provide a request-specific i18n configuration for Server Components: ` Without i18n Routing Setting up an app without i18n routing integration can be advantageous in scenarios where you want to provide a locale to next-intl based on user-specific settings or when your app supports only a single language. This approach offers the simplest way to begin using next-intl, as it requires no changes to your app’s structure, making it an ideal choice for straightforward implementations. ` Here’s a quick explanation of each file's role: * translations/: Stores different translations per language (e.g., en.json for English, es.json for Spanish). Organize this as needed, e.g., translations/en/common.json. * request.ts: Manages locale-based configuration scoped to each request. Setup request.ts for Request-Specific Configuration Since we will be using features from next-intl in Server Components, we need to add the following configuration in i18n/request.ts: ` Here, we define a static locale and use that to determine which translation file to import. The imported JSON data is stored in the message variable, and is returned together with the locale so that we can access them from various components in the application. Using Translation in RootLayout Inside RootLayout, we use getLocale() to retrieve the static locale and set the document language for SEO and pass translations to NextIntlClientProvider: ` Note that NextIntlClientProvider automatically inherits configuration from i18n/request.ts here, but messages must be explicitly passed. Now you can use translations and other functionality from next-intl in your components: ` In case of async components, you can use the awaitable getTranslations function instead: ` And with that, you have i18n configured and working on your application! \ Now, let’s take it a step further by introducing routing. \ With i18n Routing To set up i18n routing, we need a file structure that separates each language configuration and translation file. Below is the recommended structure: ` We updated the earlier structure to include some files that we require for routing: * routing.ts: Sets up locales, default language, and routing, shared between middleware and navigation. * middleware.ts: Handles URL rewrites and locale negotiation. * app/[locale]/: Creates dynamic routes for each locale like /en/about and /es/about. Define Routing Configuration in i18n/routing.ts The routing.ts file configures supported locales and the default locale, which is referenced by middleware.ts and other navigation functions: ` This configuration lets Next.js handle URL paths like /about, with locale management managed by next-intl. Update request.ts for Request-Specific Configuration We need to update the getRequestConfig function from the above implementation in i18n/request.ts. ` Here, request.ts ensures that each request loads the correct translation files based on the user’s locale or falls back to the default. Setup Middleware for Locale Matching The middleware.ts file matches the locale based on the request: ` Middleware handles locale matches and redirects to localized paths like /en or /es. Updating the RootLayout file Inside RootLayout, we use the locale from params (matched by middleware) instead of calling getLocale() ` The locale we get from the params was matched in the middleware.ts file and we use that here to set the document language for SEO purposes. Additionally, we used this file to pass configuration from i18n/request.ts to Client Components through NextIntlClientProvider. Note: When using the above setup with i18n routing, next-intl will currently opt into dynamic rendering when APIs like useTranslations are used in Server Components. next-intl provides a temporary API that can be used to enable static rendering. Static Rendering for i18n Routes For apps with dynamic routes, use generateStaticParams to pass all possible locale values, allowing Next.js to render at build time: ` next-intl provides an API setRequestLocale that can be used to distribute the locale that is received via params in layouts and pages for usage in all Server Components that are rendered as part of the request. You need to call this function in every layout/page that you intend to enable static rendering for since Next.js can render layouts and pages independently. ` Note: Call setRequestLocale before invoking useTranslations or getMessages or any next-intl functions. Domain Routing For domain-specific locale support, use the domains setting to map domains to locales, such as us.example.com/en or ca.example.com/fr. ` This setup allows you to serve localized content based on domains. Read more on domain routing here. Conclusion Setting up internationalization in Next.js with next-intl provides a modular way to handle URL-based routing, user-defined locales, and domain-specific configurations. Whether you need URL-based routing or a straightforward single-locale setup, next-intl adapts to fit diverse i18n needs. With these tools, your app will be ready to deliver a seamless multi-language experience to users worldwide....

Understanding Vue.js's <Suspense> and Async Components cover image

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly. Without further ado, let’s get started! Suspense Let's kick off by explaining what Suspense components are. They are a new component that helps manage how your application handles components that need to await for some async resource to resolve, like fetching data from a server, waiting for images to load, or any other task that might take some time to complete before they can be properly rendered. Imagine you're building a web page that needs to load data from a server, and you have 2 components that fetch the data you need as they will show different things. Typically, you might see a loading spinner or a skeleton while the data is being fetched. Suspense components make it easier to handle these scenarios. Instead of manually managing loading states and error messages for each component that needs to fetch data, Suspense components let you wrap all these components together. Inside this wrapper, you can define: 1. What to show while the data is loading (like a loading spinner). 2. The actual content that should be displayed once the data is successfully fetched. This way, Vue Suspense simplifies the process of handling asynchronous operations (like data fetching) and improves the user (and the developer) experience by providing a more seamless and integrated way to show loading states and handle errors. There are two types of async dependencies that can wait on: - Components with an async setup() hook. This includes components using with top-level await expressions. *Note: These can only be used within a component.* - Async Components. Async components Vue's asynchronous components are like a smart loading system for your web app. Imagine your app as a big puzzle. Normally, you'd put together all the pieces at once, which can take time. But what if some pieces aren't needed right away? Asynchronous components help with this. Here's how they work: - Load Only What's Needed: Just like only picking up puzzle pieces you need right now, asynchronous components let your app load only the parts that are immediately necessary. Other parts can be loaded later, as needed. - Faster Start: Your app starts up faster because it doesn't have to load everything at once. It's like quickly starting with the border of a puzzle and filling in the rest later. - Save Resources: It uses your web resources (like internet data) more wisely, only grabbing what’s essential when it's essential. In short, asynchronous components make your app quicker to start and more efficient, improving the overall experience for your users. Example: ` Combining Async Components and Suspense Let's explore how combining asynchronous components with Vue's Suspense feature can enhance your application. When asynchronous components are used with Vue's Suspense, they form a powerful combination. The key point is that async components are "suspensable" by default. This means they can be easily integrated with Suspense to improve how your app handles loading and rendering components. When used together, you can do the following things: - Centralized Loading and Error Handling: With Suspense, you don't have to handle loading and error states individually for each async component. Instead, you can define a single loading indicator or error message within the Suspense component. This unified approach simplifies your code and ensures consistency across different parts of your app. - Flexible and Clean Code Structure: By combining async components with Suspense, your code becomes more organized and easier to maintain. An asynchronous component has the flexibility to operate independently of Suspense's oversight. By setting suspensible: false in its options, the component takes charge of its own loading behavior. This means that instead of relying on Suspense to manage when it appears, the component itself dictates its loading state and presentation. This option is particularly useful for components that have specific loading logic or visuals they need to maintain, separate from the broader Suspense-driven loading strategy in the application. In practice, this combo allows you to create a user interface that feels responsive and cohesive. Users see a well-timed loading indicator while the necessary components are being fetched, and if something goes wrong, a single, well-crafted error message is displayed. It's like ensuring that the entire puzzle is either revealed in its completed form or not at all rather than showing disjointed parts at different times. How it works When a component inside the boundary is waiting for something asynchronous, shows fallback content. This fallback content can be anything you choose, such as a loading spinner or a message indicating that data is being loaded. Example Usage Let’s use a simple example: In the visual example provided, imagine we have two Vue components: one showcasing a selected Pokémon, Eevee, and a carousel showcasing a variety of other Pokémon. Both components are designed to fetch data asynchronously. Without , while the data is being fetched, we would typically see two separate loading indicators: one for the Eevee Pokemon that is selected and another for the carousel. This can make the page look disjointed and be a less-than-ideal user experience. We could display a single, cohesive loading indicator by wrapping both components inside a boundary. This unified loading state would persist until all the data for both components—the single Pokémon display and the carousel—has been fetched and is ready to be rendered. Here's how you might structure the code for such a scenario: ` Here, is the component that's performing asynchronous operations. While loading, the text 'Loading...' is displayed to the user. Great! But what about when things don't go as planned and an error occurs? Currently, Vue's doesn't directly handle errors within its boundary. However, there's a neat workaround. You can use the onErrorCaptured() hook in the parent component of to catch and manage errors. Here's how it works: ` If we run this code, and let’s say that we had an error selecting our Pokemon, this is how it is going to display to the user: The error message is specifically tied to the component where the issue occurred, ensuring that it's the only part of your application that shows an error notification. Meanwhile, the rest of your components will continue to operate and display as intended, maintaining the overall user experience without widespread disruption. This targeted error handling keeps the application's functionality intact while indicating where the problem lies. Conclusion stands out as a formidable feature in Vue.js, transforming the management of asynchronous operations into a more streamlined and user-centric process. It not only elevates the user experience by ensuring smoother interactions during data loading phases but also enhances code maintainability and application performance. I hope you found this blog post enlightening and that it adds value to your Vue.js projects. As always, happy coding and continue to explore the vast possibilities Vue.js offers to make your applications more efficient and engaging!...

What does it actually look like to build software with AI today? Not in theory, but in practice. cover image

What does it actually look like to build software with AI today? Not in theory, but in practice.

What does it actually look like to build software with AI today? Not in theory, but in practice. At the Leadership Exchange, this was the question at the center of the Developer Panel, where leaders from across the industry unpacked what’s really changing inside engineering teams and what organizations need to do right now to keep up. The Developer Panel at the Leadership Exchange explored the cutting edge of AI in software engineering and examined what organizations should focus on today to prepare for the future. Moderated by Jeff Cross, Co-Founder & CEO at Nx, the panel featured Victor Savkin, Cofounder & CTO at Nx, Alex Sover, Vice President of Engineering at OpenAP, Brent Zucker, Senior Director of Engineering at Visa, and Jonathan Fontanez, AI Engineering Lead at This Dot Labs. Panelists shared insights into how AI is transforming the software development lifecycle and how teams can adopt tools effectively while preparing for organizational change. Panelists discussed emerging workflows, including CI-in-the-loop, agentic healing, and context engineering. They examined how validation, code reviews, and PRDs are evolving alongside AI capabilities and how teams are integrating external sources such as production traces to improve quality and reliability. The discussion also covered what the next generation of agentic tools might look like and how these capabilities will shape engineering practices in the near future. Adoption of AI comes with challenges. Teams often rely on plugins or extensions without foundational understanding, and individual contributors may fear displacement. Panelists emphasized that education, governance, and skill-building are essential for teams to manage AI agents effectively while maintaining quality. They also highlighted the need to standardize workflows and ensure organizational alignment to fully leverage AI capabilities. The conversation extended beyond technical challenges to organizational implications. Panelists discussed how teams can avoid issues like Conway’s Law, manage distributed teams effectively, and evolve engineering practices alongside AI adoption. Leadership and management strategies play a crucial role in ensuring that AI integration delivers meaningful outcomes while maintaining efficiency and alignment with business objectives. Key Takeaways - AI workflows require both technical and organizational preparation. - Education, governance, and skill development are essential for successful implementation. - Forward-looking teams are rethinking validation, CI pipelines, and context management to fully leverage agentic AI. The discussion highlighted that adopting AI at the cutting edge is not just about new tools - it is about rethinking processes, workflows, and organizational culture. Companies that embrace this holistic approach are most likely to succeed in leveraging AI to its full potential. Are you interested in more conversations like this? Message us for an invite to the next, or for a private discussion around these topics. Tracy can be reached at tlee@thisdot.co....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co