<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Neon</title>
    <description>The latest articles on DEV Community by Neon (@neon-postgres).</description>
    <link>https://dev.to/neon-postgres</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F8214%2F1dedca6f-d508-49c2-8b23-220904e7dbe7.jpeg</url>
      <title>DEV Community: Neon</title>
      <link>https://dev.to/neon-postgres</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neon-postgres"/>
    <language>en</language>
    <item>
      <title>Interact with Neon APIs using Claude Code through natural language</title>
      <dc:creator>Pedro Figueiredo</dc:creator>
      <pubDate>Fri, 29 Aug 2025 15:20:38 +0000</pubDate>
      <link>https://dev.to/neon-postgres/interact-with-neon-apis-using-claude-code-through-natural-language-267e</link>
      <guid>https://dev.to/neon-postgres/interact-with-neon-apis-using-claude-code-through-natural-language-267e</guid>
      <description>&lt;p&gt;Imagine adjusting your database schema simply by describing the change in plain English. This is possible by combining &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; with the &lt;a href="https://github.com/neondatabase/mcp-server-neon" rel="noopener noreferrer"&gt;Neon MCP Server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This guide demonstrates how to use Claude Code's command-line interface and Neon's MCP server to perform database migrations in your Neon project.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
The Neon MCP Server grants powerful database management capabilities through natural language requests. &lt;strong&gt;Always review and authorize actions requested by the LLM before execution.&lt;/strong&gt; Ensure that only authorized users and applications have access to the Neon MCP Server.&lt;/p&gt;

&lt;p&gt;The Neon MCP Server is intended for local development and IDE integrations only. &lt;strong&gt;We do not recommend using the Neon MCP Server in production environments.&lt;/strong&gt; It can execute powerful operations that may lead to accidental or unauthorized changes.&lt;/p&gt;

&lt;p&gt;For more information, see &lt;a href="https://neon.com/docs/ai/neon-mcp-server#mcp-security-guidance" rel="noopener noreferrer"&gt;MCP security guidance&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Key components
&lt;/h2&gt;

&lt;p&gt;Let's break down the key components in this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: Claude Code is Anthropic's official CLI tool that supports Model Context Protocol (MCP) for interfacing with external tools (APIs, databases, etc.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Neon MCP Server&lt;/strong&gt;: Neon's MCP server acts as a bridge between MCP clients like Claude Code and &lt;a href="https://api-docs.neon.tech/reference/getting-started-with-neon-api" rel="noopener noreferrer"&gt;Neon's API&lt;/a&gt;, letting you work with Neon databases using natural language commands.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;: MCP is a lightweight communication standard that allows Claude Code and Neon MCP Server to work together.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up Neon MCP Server in Claude Code
&lt;/h2&gt;

&lt;p&gt;You have two options for connecting Claude Code to the Neon MCP Server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remote MCP Server (Preview):&lt;/strong&gt; Connect to Neon's managed MCP server using OAuth for authentication. This method is more convenient as it eliminates the need to manage API keys in Claude Code. Additionally, you will automatically receive the latest features and improvements as soon as they are released.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local MCP Server:&lt;/strong&gt; Run the Neon MCP server locally on your machine, authenticating with a Neon API key.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before you begin, ensure you have the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code:&lt;/strong&gt; Ensure you have Claude Code installed. Visit &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;docs.anthropic.com/en/docs/claude-code&lt;/a&gt; for installation instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon API Key (for Local MCP server):&lt;/strong&gt; After signing up, get your Neon API Key from the &lt;a href="https://console.neon.tech/app/settings/api-keys" rel="noopener noreferrer"&gt;Neon console&lt;/a&gt;. This API key is needed to authenticate your application with Neon. For instructions, see &lt;a href="https://neon.com/docs/manage/api-keys" rel="noopener noreferrer"&gt;Manage API keys&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br&gt;
   Keep your Neon API key secure, and never share it publicly. It provides access to your Neon projects.&lt;br&gt;
   &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node.js (&amp;gt;= v18) and npm:&lt;/strong&gt; Ensure Node.js (version 18 or later) and npm are installed. Download them from &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;nodejs.org&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Option 1: Setting up the Remote Hosted Neon MCP Server
&lt;/h3&gt;

&lt;p&gt;Note: By default, the Remote MCP Server connects to your personal Neon account. To connect to an organization's account, you must authenticate with an API key. For more information, see &lt;a href="https://neon.com/docs/ai/neon-mcp-server#api-key-based-authentication" rel="noopener noreferrer"&gt;API key-based authentication&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;This method uses Neon's managed server and OAuth authentication.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your terminal.&lt;/li&gt;
&lt;li&gt;Add the Neon MCP server to Claude Code with the following command:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http neon https://mcp.neon.tech/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start a new session of &lt;code&gt;claude&lt;/code&gt; to trigger the OAuth authentication flow:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;You can also trigger authentication with &lt;code&gt;/mcp&lt;/code&gt; within Claude Code.&lt;/li&gt;
&lt;/ol&gt;



&lt;p&gt;If you prefer to authenticate using a Neon API key, provide the &lt;code&gt;Authorization&lt;/code&gt; header to the &lt;code&gt;mcp add&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http neon https://mcp.neon.tech/mcp &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;YOUR_NEON_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;YOUR_NEON_API_KEY&amp;gt;&lt;/code&gt; with your actual Neon API key which you obtained from the prerequisites section.&lt;/p&gt;



&lt;p&gt;&lt;br&gt;
The remote hosted MCP server is in preview due to the &lt;a href="https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/" rel="noopener noreferrer"&gt;new OAuth MCP specification&lt;/a&gt;, expect potential changes as we continue to refine the OAuth integration.&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Setting up the Local Neon MCP Server
&lt;/h3&gt;

&lt;p&gt;This method runs the Neon MCP server locally on your machine, using a Neon API key for authentication.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your terminal.&lt;/li&gt;
&lt;li&gt;Add the Neon MCP server to Claude Code with the following command, replacing &lt;code&gt;&amp;lt;YOUR_NEON_API_KEY&amp;gt;&lt;/code&gt; with your actual Neon API key:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   claude mcp add neon &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @neondatabase/mcp-server-neon start &lt;span class="s2"&gt;"&amp;lt;YOUR_NEON_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;&amp;lt;YOUR_NEON_API_KEY&amp;gt;&lt;/code&gt; with your actual Neon API key which you obtained from the prerequisites section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Start a new Claude Code session with the &lt;code&gt;claude&lt;/code&gt; command and start using the Neon MCP server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've now configured the Neon MCP Server in Claude Code and can manage your Neon Postgres databases using AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification
&lt;/h3&gt;

&lt;p&gt;Now that you have the Neon MCP server set up either remotely or locally, you can verify the connection and test the available tools.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start Claude Code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Type &lt;code&gt;/mcp&lt;/code&gt; to see the available MCP servers and tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try out a Neon MCP Server tool by typing a query like &lt;code&gt;List my Neon projects&lt;/code&gt; to see your projects and verify the connection.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Supported Actions (Tools)
&lt;/h2&gt;

&lt;p&gt;The Neon MCP Server provides the following actions, which are exposed as "tools" to MCP Clients. You can use these tools to interact with your Neon projects and databases using natural language commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project management:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;list_projects&lt;/code&gt;: Retrieves a list of your Neon projects, providing a summary of each project associated with your Neon account. Supports limiting the number of projects returned (default: 10).&lt;br&gt;
&lt;code&gt;describe_project&lt;/code&gt;: Fetches detailed information about a specific Neon project, including its ID, name, and associated branches and databases.&lt;br&gt;
&lt;code&gt;create_project&lt;/code&gt;: Creates a new Neon project in your Neon account. A project acts as a container for branches, databases, roles, and computes.&lt;br&gt;
&lt;code&gt;delete_project&lt;/code&gt;: Deletes an existing Neon project and all its associated resources.&lt;br&gt;
&lt;code&gt;list_organizations&lt;/code&gt;: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch management:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;create_branch&lt;/code&gt;: Creates a new branch within a specified Neon project. Leverages Neon's branching feature for development, testing, or migrations.&lt;br&gt;
&lt;code&gt;delete_branch&lt;/code&gt;: Deletes an existing branch from a Neon project.&lt;br&gt;
&lt;code&gt;describe_branch&lt;/code&gt;: Retrieves details about a specific branch, such as its name, ID, and parent branch.&lt;br&gt;
&lt;code&gt;list_branch_computes&lt;/code&gt;: Lists compute endpoints for a project or specific branch, including compute ID, type, size, and autoscaling information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL query execution:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;get_connection_string&lt;/code&gt;: Returns your database connection string.&lt;br&gt;
&lt;code&gt;run_sql&lt;/code&gt;: Executes a single SQL query against a specified Neon database. Supports both read and write operations.&lt;br&gt;
&lt;code&gt;run_sql_transaction&lt;/code&gt;: Executes a series of SQL queries within a single transaction against a Neon database.&lt;br&gt;
&lt;code&gt;get_database_tables&lt;/code&gt;: Lists all tables within a specified Neon database.&lt;br&gt;
&lt;code&gt;describe_table_schema&lt;/code&gt;: Retrieves the schema definition of a specific table, detailing columns, data types, and constraints.&lt;br&gt;
&lt;code&gt;list_slow_queries&lt;/code&gt;: Identifies performance bottlenecks by finding the slowest queries in a database. Requires the pg_stat_statements extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database migrations (schema changes)&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;prepare_database_migration&lt;/code&gt;: Initiates a database migration process. Critically, it creates a temporary branch to apply and test the migration safely before affecting the main branch.&lt;br&gt;
&lt;code&gt;complete_database_migration&lt;/code&gt;: Finalizes and applies a prepared database migration to the main branch. This action merges changes from the temporary migration branch and cleans up temporary resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query performance tuning:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;explain_sql_statement&lt;/code&gt;: Analyzes a SQL query and returns detailed execution plan information to help understand query performance.&lt;br&gt;
&lt;code&gt;prepare_query_tuning&lt;/code&gt;: Identifies potential performance issues in a SQL query and suggests optimizations. Creates a temporary branch for testing improvements.&lt;br&gt;
&lt;code&gt;complete_query_tuning&lt;/code&gt;: Finalizes and applies query optimizations after testing. Merges changes from the temporary tuning branch to the main branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neon Auth:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;provision_neon_auth&lt;/code&gt;: Provisions Neon Auth for a Neon project. Sets up authentication infrastructure by creating an integration with Stack Auth (&lt;a class="mentioned-user" href="https://dev.to/stackframe"&gt;@stackframe&lt;/a&gt;/stack).&lt;br&gt;
These actions enable any MCP client like Claude Code to interact with various functionalities of the Neon platform via the Neon API. Certain tools, especially database migration ones, are tailored for AI agent and LLM usage, leveraging Neon's branching for safe preview and commit.&lt;/p&gt;

&lt;p&gt;These actions enable any MCP client like Claude Code to interact with various functionalities of the &lt;strong&gt;Neon platform via the Neon API.&lt;/strong&gt; Certain tools, especially database migration ones, are tailored for AI agent and LLM usage, leveraging Neon's branching for safe preview and commit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Development Workflow: Adding a Column with Claude Code and Neon MCP
&lt;/h2&gt;

&lt;p&gt;Let's walk through a typical development scenario: Quickly adding a column for prototyping using natural language within Claude Code. The following chat log demonstrates a real interaction with Claude Code and the Neon MCP server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; During development, you decide to track timestamps for entries in your &lt;code&gt;playing_with_neon&lt;/code&gt; table. You want to quickly add a &lt;code&gt;created_at&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
For your security, review the tool's purpose before permitting the operation to proceed. Remember that LLMs can sometimes produce unexpected results, so careful monitoring is always recommended.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Here's the conversation log between the user and Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: In my neon project id: round-salad-44063611 list all the tables
Claude Code: I'll help you list all the tables in your Neon project. I'll use the default database name 'neondb' since none was specified.
&amp;gt; Called MCP Tool (get_database_tables)
&amp;gt; I found one table in your database:
&amp;gt; playing_with_neon (in the public schema)
&amp;gt; Would you like to see more details about this table's structure or perform any other operations with it?

User: Can you show me first 5 rows?
Claude Code: I'll query the first 5 rows from the playing_with_neon table.
&amp;gt; Called MCP Tool (run_sql)
&amp;gt; Here are the first 5 rows from the table. The table has three columns:
&amp;gt; id (sequential numbers)
&amp;gt; name (string values)
&amp;gt; value (decimal numbers)
&amp;gt; Would you like to perform any other operations with this data?

User: can you add a created_at column?
Claude Code: I'll help you add a created_at column to the playing_with_neon table. I'll set it up with a timestamp data type and set it to automatically record the creation time of new rows.
&amp;gt; Called MCP Tool (prepare_database_migration)
&amp;gt; Let me verify the change in the temporary branch to make sure it was applied correctly.
&amp;gt; Called MCP Tool (run_sql)
&amp;gt; I've added the created_at column to the playing_with_neon table in a temporary branch. Would you like to commit this migration to the production branch?
&amp;gt; Migration Details:
&amp;gt; Migration ID: a65babcd-d1cc-401f-89b5-762570570102
&amp;gt; Temporary Branch Name: br-silent-cherry-a1eqrl6i
&amp;gt; Temporary Branch ID: br-silent-cherry-a1eqrl6i
&amp;gt; Migration Result: Success
&amp;gt; Please let me know if you'd like to proceed with applying this change to the production branch.

User: yeah do it
Claude Code: I'll complete the migration and apply the changes to the production branch.
&amp;gt; Called MCP Tool (complete_database_migration)
&amp;gt; The migration has been successfully completed! The created_at column has been added to your table in the production branch, and the temporary branch has been cleaned up.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natural language interaction:&lt;/strong&gt; You can use simple, conversational English to interact with your database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step-by-step guidance:&lt;/strong&gt; Claude Code confirms each step and provides details like branch names and migration IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Tools in Action:&lt;/strong&gt; The underlying MCP tools (&lt;code&gt;get_database_tables&lt;/code&gt;, &lt;code&gt;run_sql&lt;/code&gt;, &lt;code&gt;prepare_database_migration&lt;/code&gt;, &lt;code&gt;complete_database_migration&lt;/code&gt;) illustrate the automated process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching for safety:&lt;/strong&gt; The agent automatically creates a temporary branch for schema changes. The user will be prompted to confirm the migration before applying it to the production branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify the changes in your Neon Console or by querying the database directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Creating a project and branch
&lt;/h2&gt;

&lt;p&gt;Beyond schema changes, you can also manage your Neon projects and branches directly within Claude Code using natural language commands. This can be useful for quickly setting up a new development or test environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a new project
&lt;/h3&gt;

&lt;p&gt;Let's say you want to create a new Neon project named "my-new-project". You can simply tell Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Create a Neon project named "my-new-project"
Claude Code: I'll help you create a new Neon project with the specified name.
&amp;gt; Called MCP Tool (create_project)
&amp;gt; Great! I've created a new Neon project for you with the name "my-new-project". Here are the important details:
&amp;gt; Project ID: orange-dawn-33739962
&amp;gt; Default branch: main
&amp;gt; Default database: neondb
&amp;gt; The project is ready to use. You can start creating tables and working with your database right away.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code will then use the &lt;code&gt;create_project&lt;/code&gt; MCP tool to initiate the project creation. It will provide you with a project ID and name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a New Branch
&lt;/h3&gt;

&lt;p&gt;Once you have a project, you can easily create new branches for development or experimentation. For example, to create a branch named "feature-x" in your "my-new-project" project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Create a branch named "feature-x"
Claude Code: Perfect! I've created a new branch named "feature-x". Here are the details:
&amp;gt; Branch ID: br-cold-mountain-a523ka2w
&amp;gt; Branch name: feature-x
&amp;gt; Parent branch ID: br-odd-pine-a5y53krm
&amp;gt; The branch has been created successfully and is ready for use.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code will use the &lt;code&gt;create_branch&lt;/code&gt; MCP tool to create the branch and provide you with the branch name and ID. Notice how we don't need to specify the project ID, as Claude Code remembers the active project context.&lt;/p&gt;

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

&lt;p&gt;Claude Code combined with the Neon MCP Server, whether using the &lt;strong&gt;Remote Hosted (Preview)&lt;/strong&gt; option or the &lt;strong&gt;Local Server&lt;/strong&gt; setup, lets you use natural language to interact with your database and take advantage of Neon's branching capabilities for fast iteration. This approach is ideal for quickly testing database ideas and making schema changes during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.org" rel="noopener noreferrer"&gt;MCP Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.com/docs" rel="noopener noreferrer"&gt;Neon Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.com/docs/manage/api-keys#creating-api-keys" rel="noopener noreferrer"&gt;Neon API Keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/neondatabase/mcp-server-neon" rel="noopener noreferrer"&gt;Neon MCP server GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



</description>
    </item>
    <item>
      <title>Build Internal Tools Better: Using Neon, StackAuth, and Vercel</title>
      <dc:creator>Sam シ</dc:creator>
      <pubDate>Thu, 10 Jul 2025 15:33:24 +0000</pubDate>
      <link>https://dev.to/neon-postgres/build-internal-tools-better-using-neon-stackauth-and-vercel-419a</link>
      <guid>https://dev.to/neon-postgres/build-internal-tools-better-using-neon-stackauth-and-vercel-419a</guid>
      <description>&lt;p&gt;Almost every tech company, from small startups to Fortune 500 enterprises relies on internal tools. Larger organizations often have dedicated services and structured procedures in place, but for many others, internal tools are... messy. They often take the form of an insecure setup hosted behind a VPN or worse, an overly complex solution cobbled together from a surprising number of often times costly AWS services.&lt;/p&gt;

&lt;p&gt;But if you’re like me, you just want to share tools easily, securely, cheaply, and without feeling like you’re managing a full-fledged production application (that's already your 9 to 5).&lt;/p&gt;

&lt;p&gt;That's what drove me to create &lt;a href="https://github.com/sam-harri/internal_tooling_neon_stack" rel="noopener noreferrer"&gt;this template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Built using Neon, StackAuth, and Vercel's free plans, it provides a secure and scalable foundation, enabling you to quickly launch your next internal tool in minutes: &lt;a href="https://github.com/sam-harri/internal_tooling_neon_stack" rel="noopener noreferrer"&gt;https://github.com/sam-harri/internal_tooling_neon_stack&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It comes preconfigured with a Postgres database, API routes, authentication, authorization, and a built-in admin panel – everything you need to get an application running fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F05d5g8aw3ajs2w7d8c91.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F05d5g8aw3ajs2w7d8c91.gif" alt="create your own internal tools with neon" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with the Template
&lt;/h2&gt;

&lt;p&gt;Using the template, you can get your internal tool up and running in minutes, and all you need are Neon, Stack Auth, and Vercel accounts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git clone https://github.com/sam-harri/internal_tooling_neon_stack.git&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then open that new project in your editor of choice, and install the dependencies: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, set up the environment variables by copying over the .env.example to a new .env file. Fill these in with the connection string from your Neon database project in the console, and the Stack Auth keys from the Auth tab.&lt;/p&gt;

&lt;p&gt;Since the template comes with some admin logic already, you’ll need to sync your database by applying the schema:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx drizzle-kit push&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up Roles and Claim Your Project
&lt;/h2&gt;

&lt;p&gt;Head to the Auth tab in the Neon console and claim the project with your Stack Auth account. From there, create two project roles: &lt;code&gt;admin&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt;, where &lt;code&gt;user&lt;/code&gt; is contained within &lt;code&gt;admin&lt;/code&gt;. Your Project Permissions section should look like this afterward:&lt;/p&gt;

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

&lt;p&gt;Once that’s done, boot up the project locally, log in, and head over to &lt;code&gt;/setup&lt;/code&gt; to claim your admin privileges.&lt;/p&gt;

&lt;p&gt;You can then deploy your app with Vercel. Don’t forget to add your Vercel-provided domain to the allowed domains in Neon’s Auth tab, or your custom domain if you're using one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication and Access Control with StackAuth
&lt;/h2&gt;

&lt;p&gt;You obviously don't want just anyone to be able to access your internal tools. Luckily, Neon comes with an auth solution supporting social and password sign-on, role-based access control, and much more.&lt;/p&gt;

&lt;p&gt;In this template, only authorized users can access the tools. The application admin can whitelist domains, allowing anyone with an email from a specific domain (e.g. &lt;code&gt;yourcompany.tld&lt;/code&gt;) and a verified email to sign up and get access. Likewise, you can grant and revoke access to specific external email addresses, perfect for contractors or partners. Emails can also be explicitly blocked even if their domain is whitelisted.&lt;/p&gt;

&lt;p&gt;Managing access is straightforward via the built-in admin panel at &lt;code&gt;/tools/admin&lt;/code&gt;. Admins can assign or revoke privileges to other users, giving you complete control over your application's access policies. These rules are meant to cover most of the common scenarios you might face, but since it’s a template, you can easily update it to add features like regex validation or subdomain whitelisting.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Database and Logic with Neon and Next.js
&lt;/h2&gt;

&lt;p&gt;Internal tools inherently require business logic and data storage. Neon provides a free serverless Postgres database, along with all the most popular Postgres extensions, and offers a serverless database driver that’s perfect for single-shot queries in Next.js serverless functions.&lt;/p&gt;

&lt;p&gt;For business logic, you can use Next’s server actions and API routes without the overhead of managing and deploying a separate API—ideal for simple internal tools. Vercel makes hosting your Next.js app painless, even allowing you to attach a custom domain so that you can host it at something like yourtool.yourcompany.tld.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customize the App for Your Team
&lt;/h2&gt;

&lt;p&gt;Customization is designed to be simple. Just fill out your company-specific details in &lt;code&gt;config/app.ts&lt;/code&gt;, then add your custom tools to the &lt;code&gt;app/tools&lt;/code&gt; directory and declare them within the configuration file. To get started, play around with &lt;code&gt;app/tools/tool1/page.tsx&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/sam-harri/internal_tooling_neon_stack" rel="noopener noreferrer"&gt;Clone the template&lt;/a&gt;, connect your Neon and StackAuth accounts, and deploy to Vercel. Your next internal tool is just a few commands away!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building Intelligent Search with AI Embeddings, Neon, and pgvector</title>
      <dc:creator>Bobby</dc:creator>
      <pubDate>Fri, 20 Jun 2025 17:21:55 +0000</pubDate>
      <link>https://dev.to/neon-postgres/building-intelligent-search-with-ai-embeddings-neon-and-pgvector-3hep</link>
      <guid>https://dev.to/neon-postgres/building-intelligent-search-with-ai-embeddings-neon-and-pgvector-3hep</guid>
      <description>&lt;p&gt;Traditional text search relies on exact keyword matches, which often misses the semantic meaning behind queries.&lt;/p&gt;

&lt;p&gt;When someone searches for "car maintenance," they might also be interested in results about "vehicle servicing" or "auto repair", but keyword-based search won't make these connections.&lt;/p&gt;

&lt;p&gt;AI embeddings solve this problem by converting text into high-dimensional vectors that capture semantic meaning. Words and phrases with similar meanings cluster together in this vector space, enabling search systems that understand context and intent rather than just matching exact words.&lt;/p&gt;

&lt;p&gt;The pgvector extension brings vector similarity search directly into PostgreSQL, letting you store embeddings alongside your regular data and perform complex semantic searches with simple SQL queries. Combined with Neon's serverless PostgreSQL, you can build intelligent search systems that scale automatically with your application's needs.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how to build a semantic search system that can power document search and content recommendations using OpenAI embeddings stored in Neon with pgvector.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An understanding of AI embeddings and how they improve search&lt;/li&gt;
&lt;li&gt;A Neon database configured with the pgvector extension&lt;/li&gt;
&lt;li&gt;An embedding generation service powered by OpenAI's API&lt;/li&gt;
&lt;li&gt;A document search system that understands semantic similarity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow along with this guide, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;Neon account&lt;/a&gt; with a project&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://platform.openai.com/api-keys" rel="noopener noreferrer"&gt;OpenAI API key&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Node.js 20.x or later installed&lt;/li&gt;
&lt;li&gt;Basic familiarity with SQL and REST APIs&lt;/li&gt;
&lt;li&gt;Understanding of JavaScript promises and async/await&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding AI embeddings and vector search
&lt;/h2&gt;

&lt;p&gt;Before diving into the implementation, let's understand how AI embeddings work and why they're great for search.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are embeddings?
&lt;/h3&gt;

&lt;p&gt;Embeddings are numerical representations of text that capture semantic meaning in high-dimensional space. When you send text to OpenAI's embedding API, it returns an array of floating-point numbers (typically 1,536 dimensions for the &lt;code&gt;text-embedding-3-small&lt;/code&gt; model) that represents the "meaning" of that text.&lt;/p&gt;

&lt;p&gt;Here's a simplified example of how embeddings work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// These texts have similar meanings&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The cat jumped over the fence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A feline leaped across a barrier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Their embeddings would be mathematically similar&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedding1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...];&lt;/span&gt; &lt;span class="c1"&gt;// 1,536 numbers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedding2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...];&lt;/span&gt; &lt;span class="c1"&gt;// Similar values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is that semantically similar texts produce similar embedding vectors. This enables search systems that can find relevant content even when it doesn't share exact keywords with the query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why pgvector?
&lt;/h3&gt;

&lt;p&gt;pgvector extends PostgreSQL with vector data types and similarity search operations. Instead of moving your data to specialized vector databases, you can store embeddings alongside your existing relational data and perform vector similarity searches with SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find documents most similar to a query embedding&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;-&amp;gt;&lt;/code&gt; operator calculates the distance between vectors, with smaller distances indicating higher similarity.&lt;/p&gt;

&lt;p&gt;Now let's build a system that puts these concepts to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up pgvector on Neon
&lt;/h2&gt;

&lt;p&gt;We'll start by enabling pgvector on your Neon database and creating the necessary tables for our search system.&lt;/p&gt;

&lt;p&gt;First, create a new Neon project optimized for vector operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;a href="https://console.neon.tech" rel="noopener noreferrer"&gt;Neon Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click "New Project"&lt;/li&gt;
&lt;li&gt;Name your project "semantic-search-system"&lt;/li&gt;
&lt;li&gt;Choose a region close to your users&lt;/li&gt;
&lt;li&gt;Select at least 1 CU for compute size (vector operations can be CPU-intensive)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once your project is created, we need to enable the pgvector extension. Connect to your database and run this SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable the pgvector extension&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Verify the extension is working&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;extversion&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_extension&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;extname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'vector'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command adds vector data types and similarity functions to your PostgreSQL database. You should see confirmation that the vector extension is now available.&lt;/p&gt;

&lt;p&gt;Next, we'll create the database schema for our semantic search system. This includes tables for documents and their embeddings, along with indexes for fast vector similarity search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Table for storing documents with their embeddings&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- OpenAI's text-embedding-3-small produces 1536-dimensional vectors&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create indexes for fast vector similarity search&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;documents_embedding_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vector(1536)&lt;/code&gt; data type stores 1,536-dimensional vectors, matching OpenAI's embedding size. The &lt;code&gt;ivfflat&lt;/code&gt; indexes enable fast approximate nearest neighbor searches using cosine similarity, this is crucial for performance when searching through thousands of embeddings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an embedding generation service
&lt;/h2&gt;

&lt;p&gt;Now we'll create a Node.js service that handles the complexities of generating embeddings with &lt;a href="https://platform.openai.com/docs/guides/embeddings" rel="noopener noreferrer"&gt;OpenAI's API&lt;/a&gt;. This service will be the bridge between your text content and the vector representations stored in the database.&lt;/p&gt;

&lt;p&gt;Let's set up the project structure and install the necessary dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;semantic-search-service
&lt;span class="nb"&gt;cd &lt;/span&gt;semantic-search-service
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;openai pg dotenv express cors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file to securely store your API credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_openai_api_key_here
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://user:password@ep-abc123.region.aws.neon.tech/neondb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's create an embedding service that handles OpenAI API interactions. This service will preprocess text, generate embeddings, and handle errors gracefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// embedding.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Cost-effective and performant&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8191&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Maximum tokens for this model&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Clean and prepare text for embedding generation&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleanText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preprocessText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cleanText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error generating embedding:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to generate embedding: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;preprocessText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Remove extra whitespace and normalize the text&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Truncate if too long (rough estimate: 1 token ≈ 4 characters)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Conservative estimate&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Create optimized text for embedding generation&lt;/span&gt;
  &lt;span class="nf"&gt;createDocumentText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Title: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nCategory: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nContent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EmbeddingService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This service handles a few important tasks: it preprocesses text to ensure it fits within OpenAI's token limits, combines document fields into optimized text for better embeddings, and provides error handling for API calls. The &lt;code&gt;createDocumentText&lt;/code&gt; method is particularly important because it structures the input text in a way that produces better semantic embeddings.&lt;/p&gt;

&lt;p&gt;As of the time of writing, OpenAI's &lt;code&gt;text-embedding-3-small&lt;/code&gt; model is the newest and most cost-effective for generating embeddings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a document management system
&lt;/h2&gt;

&lt;p&gt;Now we'll build a service that manages documents in our database and automatically generates embeddings for each document. This service will handle both individual documents and batch operations.&lt;/p&gt;

&lt;p&gt;Let's create the document service that ties together our database and embedding generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// document-service.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EmbeddingService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./embedding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocumentService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddingService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;addDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Combine document fields into optimized text for embedding&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocumentText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Generate embedding using OpenAI API&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Generating embedding for document: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Store document with its embedding in the database&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`
        INSERT INTO documents (title, content, category, embedding)
        VALUES ($1, $2, $3, $4)
        RETURNING id, title, created_at
      `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formattedEmbedding&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Complete transaction&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Document added successfully: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Undo changes on error&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error adding document:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Return connection to pool&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;searchDocuments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// First, convert the search query into an embedding&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocumentText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Format embedding as a pgvector-compatible string&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Start building the SQL query&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
            SELECT 
                id,
                title,
                content,
                category,
                created_at,
                1 - (embedding &amp;lt;=&amp;gt; $1) as similarity_score
            FROM documents
            WHERE 1 - (embedding &amp;lt;=&amp;gt; $1) &amp;gt; 0.3
            `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;formattedEmbedding&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

      &lt;span class="c1"&gt;// Add category filter if needed&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;` WHERE category = $2`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Append ORDER BY and LIMIT clauses&lt;/span&gt;
      &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;` ORDER BY embedding &amp;lt;=&amp;gt; $1 LIMIT $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Format results&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;similarity_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;similarity_score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="na"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error searching documents:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getDocumentById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`
        SELECT id, title, content, category, created_at
        FROM documents 
        WHERE id = $1
      `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error getting document:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DocumentService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This service provides the core functionality for our semantic search system. The &lt;code&gt;addDocument&lt;/code&gt; method generates embeddings and stores them alongside the document data, while &lt;code&gt;searchDocuments&lt;/code&gt; performs the actual semantic search by converting queries to embeddings and finding the most similar documents using pgvector's distance operators.&lt;/p&gt;

&lt;p&gt;The key insight here is the &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; operator in the SQL query, this calculates cosine distance between vectors, with smaller values indicating higher similarity. We convert this to a similarity score between 0 and 1 for easier interpretation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;getDocumentById&lt;/code&gt; method retrieves a specific document by its ID, allowing applications to fetch full content when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the search API
&lt;/h2&gt;

&lt;p&gt;With our document service in place, let's create an Express API that exposes our document search capabilities to applications.&lt;/p&gt;

&lt;p&gt;This API will provide endpoints for adding documents and performing semantic searches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DocumentService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./document-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documentService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DocumentService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Add a single document with automatic embedding generation&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/documents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate required fields&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Title and content are required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Add document and generate embedding&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;documentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error adding document:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to add document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Perform semantic search across all documents&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search query is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Execute semantic search using vector similarity&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;documentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;searchDocuments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;search_time_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error searching documents:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Get a specific document by ID&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/documents/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;documentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocumentById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Document not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error getting document:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to get document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Health check endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Test database connection&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;documentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhealthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disconnected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Semantic search service running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API provides clean, RESTful endpoints for our semantic search functionality. The &lt;code&gt;/search&lt;/code&gt; endpoint is the heart of the system, it takes natural language queries and returns semantically relevant documents, even when there are no exact keyword matches.&lt;/p&gt;

&lt;p&gt;Notice how we measure and return the search time. This helps you monitor performance as your document collection grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the semantic search system
&lt;/h2&gt;

&lt;p&gt;With our API in place, now let's test our semantic search system to see how it finds relevant documents based on meaning rather than exact keywords. We'll add sample documents and run various search queries.&lt;/p&gt;

&lt;p&gt;Create a test script to populate your database with sample content and test the search functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test-search.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;testSemanticSearch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Adding sample documents to test semantic search...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add diverse sample documents across different topics&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sampleDocuments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Getting Started with Machine Learning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Machine learning is a subset of artificial intelligence that enables computers to learn and make decisions from data without being explicitly programmed. This guide covers the basics of supervised learning, unsupervised learning, and neural networks.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;technology&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Healthy Cooking Tips&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Eating nutritious meals doesn't have to be complicated. Focus on fresh ingredients, reduce processed foods, and try cooking methods like steaming and grilling. Meal prep can save time and help maintain a balanced diet.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Remote Work Best Practices&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Working from home requires discipline and good habits. Set up a dedicated workspace, maintain regular hours, and use collaboration tools effectively. Communication with team members is crucial for remote success.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;productivity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Understanding Neural Networks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Neural networks are computing systems inspired by biological neural networks. They consist of layers of interconnected nodes that process information. Deep learning uses multi-layered neural networks to solve complex problems.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;technology&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Add each document to the system&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;sampleDocuments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/documents`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✓ Added: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Documents added successfully! Now testing semantic search...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Test semantic searches that don't use exact keywords&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testQueries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;artificial intelligence and computers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Should find ML and neural network docs&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;how to stay healthy while eating&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Should find cooking tips&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;working from home effectively&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Should find remote work practices&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep learning algorithms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Should find both AI-related documents&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Run each test query and display results&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;testQueries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`🔍 Searching for: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/search`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search_time_ms&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; results in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;search_time_ms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Display the most relevant results&lt;/span&gt;
      &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (similarity: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;similarity_score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`      Category: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`      Preview: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Start the server and run tests&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Make sure your server is running with: node server.js&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;testSemanticSearch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install &lt;code&gt;axios&lt;/code&gt; for the test script and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;axios

&lt;span class="c"&gt;# Start your server in one terminal&lt;/span&gt;
node server.js

&lt;span class="c"&gt;# Run the test in another terminal&lt;/span&gt;
node test-search.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output showing how the semantic search finds relevant documents even when the search terms don't appear exactly in the document text.&lt;/p&gt;

&lt;p&gt;For example, searching for "artificial intelligence and computers" should return documents about machine learning and neural networks, demonstrating the power of semantic understanding.&lt;/p&gt;

&lt;p&gt;The similarity scores help you understand how closely each result matches the query - scores closer to 1.0 indicate higher semantic similarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance optimization
&lt;/h2&gt;

&lt;p&gt;As your document collection grows, you'll want to optimize performance.&lt;/p&gt;

&lt;p&gt;First, let's properly configure the pgvector indexes. The default index settings work for small datasets, but you'll need to tune them for larger collections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Drop existing indexes to recreate with optimal settings&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;documents_embedding_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create optimized indexes based on your data size&lt;/span&gt;
&lt;span class="c1"&gt;-- Rule of thumb: lists = rows / 1000, with minimum of 10&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;documents_embedding_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- Adjust based on your document count&lt;/span&gt;

&lt;span class="c1"&gt;-- Update table statistics for optimal query planning&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These indexes dramatically improve query speed by creating approximate nearest neighbor searches instead of comparing every vector.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;lists&lt;/code&gt; parameter should be adjusted based on your data size, more documents need more lists for optimal performance.&lt;/p&gt;

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

&lt;p&gt;You now should have a solid foundation for a semantic search system that uses AI embeddings alongside &lt;code&gt;pgvector&lt;/code&gt; in Neon.&lt;/p&gt;

&lt;p&gt;You've built a simple semantic search system that demonstrates the power of AI embeddings with Neon and &lt;code&gt;pgvector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The foundation you've built can be extended with features like real-time search suggestions or multilingual support. The principles of semantic search and vector similarity will enable you to create intelligent applications that understand user intent and context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/embeddings" rel="noopener noreferrer"&gt;OpenAI Embeddings Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.com//docs" rel="noopener noreferrer"&gt;Neon Serverless PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pgvector/pgvector#best-practices" rel="noopener noreferrer"&gt;Vector Similarity Search Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



</description>
    </item>
    <item>
      <title>app.build – Open-Source AI Agent That Builds Full-Stack Apps</title>
      <dc:creator>David Gomes</dc:creator>
      <pubDate>Wed, 04 Jun 2025 20:41:52 +0000</pubDate>
      <link>https://dev.to/neon-postgres/appbuild-open-source-ai-agent-that-builds-full-stack-apps-1cmg</link>
      <guid>https://dev.to/neon-postgres/appbuild-open-source-ai-agent-that-builds-full-stack-apps-1cmg</guid>
      <description>&lt;p&gt;Code generation has been one of the most interesting use cases for LLMs. While the best models can generate decent code for isolated problems, there is a big gap between these code snippets and fully functional applications. Agent-based solutions are better suited to create apps. To close the gap, an agent needs to iterate on the code, test the result and make decisions based on the feedback - either human comments or technical (like logs, test results, linters output etc.). In the last few months, a lot of AI app builders have come to market to tackle this problem, but the gap is not closed yet.&lt;/p&gt;

&lt;p&gt;And today, Neon is launching app.build - an open-source agent that can build and deploy full-stack applications, with end-to-end tests and automated deployments. We want to build a community around this project, since we know a lot of developers are interested in hacking in this space, particularly by bringing their own models and doing most things locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I try it then?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx @app.build/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This very simple command is all you need to get started. Notice that, by default, it will ask you to sign in with GitHub. Any application that you generate will have its own repository (in your GitHub account), and will be deployed to the Internet with a real backend and a real database. This is our "managed service" experience. We're also working on a smoother way for you to run everything locally (which will include being able to choose which models you want for which tasks, and even self-hosted models).&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I run this locally?
&lt;/h2&gt;

&lt;p&gt;Yes, everything runs locally. Our &lt;a href="https://dealsbe.com/" rel="noopener noreferrer"&gt;README.md&lt;/a&gt; contains instructions for how to run this locally, but we have yet to create a full guide for running both the CLI and the agent locally with more detail.&lt;/p&gt;

&lt;p&gt;We also want to write down instructions on how you can bring your own models (including self-hosted ones).&lt;/p&gt;

&lt;h2&gt;
  
  
  What features does the platform offer?
&lt;/h2&gt;

&lt;p&gt;Our CLI allows both creating new apps from scratch, and iterating on previously created apps (add new features or other types of changes). It is extremely barebones for now!&lt;/p&gt;

&lt;p&gt;But our platform gives you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An authentication provider (it defaults to Neon Auth)&lt;/li&gt;
&lt;li&gt;A hosted database (it defaults to Neon Postgres)&lt;/li&gt;
&lt;li&gt;Your app's frontend and backend get deployed immediately (in Neon and Koyeb's infrastructure)&lt;/li&gt;
&lt;li&gt;Hosted repository on your own GitHub account&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How does the agent work?
&lt;/h2&gt;

&lt;p&gt;We've taken a lot of steps towards having an agent architecture that is focused on generating really high-quality applications that actually work as intended. This is why our agent writes end-to-end tests and actually runs them to succession as part of the generation pipeline.&lt;/p&gt;

&lt;p&gt;Furthermore, we have a very solid base template for every app (for now, just 1 template, which is for web apps), and we made sure that the agent is extremely well-versed on all the technologies that are used in this template (Fastify, Drizzle, React, Vite, etc.).&lt;/p&gt;

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

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

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

&lt;p&gt;Our agent follows the "divide-and-conquer" principle — app creation is decomposed into multiple tasks, and each can be solved independently. Furthermore, those tasks themselves may have their own subtasks, so we don't rely on LLMs generating large chunks of code at once. Each particular task passes a series of checks (e.g. Does it compile? Does it pass ESLint? Does it pass tests?), thus ensuring the final app works.&lt;/p&gt;

&lt;p&gt;In future blog posts and talks, we'll definitely dive deeper into the inner workings of the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I get involved?
&lt;/h2&gt;

&lt;p&gt;For now, we're hacking over at &lt;a href="https://github.com/appdotbuild" rel="noopener noreferrer"&gt;github.com/appdotbuild&lt;/a&gt;. You can see all the code, all the commits and all of our planned tasks. We hope to create a Discord channel, and a proper public-facing roadmap soon!&lt;/p&gt;

&lt;p&gt;We're also currently working on more blog posts and videos explaining our code generation pipeline, and other architectural decisions we made while building the agent.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Comparing Text Search Strategies pg_search vs. tsvector vs. External Engines</title>
      <dc:creator>Rishi Raj Jain</dc:creator>
      <pubDate>Thu, 08 May 2025 17:01:53 +0000</pubDate>
      <link>https://dev.to/neon-postgres/comparing-text-search-strategies-pgsearch-vs-tsvector-vs-external-engines-54f0</link>
      <guid>https://dev.to/neon-postgres/comparing-text-search-strategies-pgsearch-vs-tsvector-vs-external-engines-54f0</guid>
      <description>&lt;p&gt;Choosing the Right Search Approach for Your Application with PostgreSQL and &lt;a href="https://neon.com" rel="noopener noreferrer"&gt;Neon Postgres.&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
When implementing search in your application, you need to choose the right text search approach. This guide compares PostgreSQL's built-in &lt;a href="https://neon.tech/guides/full-text-search" rel="noopener noreferrer"&gt;&lt;code&gt;tsvector&lt;/code&gt;&lt;/a&gt;, the &lt;a href="https://dev.todocs/extensions/pg_search"&gt;&lt;code&gt;pg_search&lt;/code&gt;&lt;/a&gt; extension, and external search engines to help you select the best option for your needs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Built-in PostgreSQL &lt;code&gt;tsvector&lt;/code&gt; for Text Search
&lt;/h2&gt;

&lt;p&gt;PostgreSQL includes native full-text search capabilities using the &lt;code&gt;tsvector&lt;/code&gt; data type and &lt;code&gt;tsquery&lt;/code&gt; search expressions. The &lt;code&gt;tsvector&lt;/code&gt; data type is a specialized data structure that PostgreSQL uses to represent documents in a form optimized for search.&lt;/p&gt;

&lt;p&gt;This built-in functionality works for basic search needs without requiring any additional extensions. It's perfect for smaller applications or when you don't require advanced search features.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example of Using &lt;code&gt;tsvector&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To use &lt;code&gt;tsvector&lt;/code&gt;, you need to create a table with a column to store the search vector. You can then convert text into the &lt;code&gt;tsvector&lt;/code&gt; format and create an index for efficient searching. This allows your queries to run faster, even as your dataset grows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Update the search vector&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create an index on the search vector&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_search_vector&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query using tsquery&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'database &amp;amp; performance'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a table with a column to store search vectors&lt;/li&gt;
&lt;li&gt;Convert text to the &lt;code&gt;tsvector&lt;/code&gt; format using the &lt;code&gt;to_tsvector&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;Create a GIN index to speed up searches&lt;/li&gt;
&lt;li&gt;Search for articles containing both "database" and "performance"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While &lt;code&gt;tsvector&lt;/code&gt; meets basic search needs, it has limitations with relevance ranking, handling typos, and complex search patterns. It’s suitable for small to medium-sized datasets where advanced features aren’t necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros of &lt;code&gt;tsvector&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;: No need for any extensions, just native PostgreSQL functionality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated with Postgres&lt;/strong&gt;: Works seamlessly within the same database, avoiding the need for additional services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low overhead&lt;/strong&gt;: Since &lt;code&gt;tsvector&lt;/code&gt; is part of Postgres, it doesn’t require a separate system to maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of &lt;code&gt;tsvector&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limited relevance ranking&lt;/strong&gt;: It doesn't automatically rank search results based on relevance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No typo tolerance&lt;/strong&gt;: Exact matches are required, so if a user misspells a search term, it won’t return relevant results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex queries&lt;/strong&gt;: Handling more complex queries like fuzzy matching or phrase proximity is not possible out-of-the-box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Extending &lt;code&gt;tsvector&lt;/code&gt; with &lt;code&gt;pg_search&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pg_search&lt;/code&gt; extension builds on PostgreSQL's search capabilities by adding better relevance ranking with the BM25 algorithm, fuzzy matching for handling typos, and more flexible search options.&lt;/p&gt;

&lt;p&gt;This extension is particularly useful for applications that require more sophisticated search features without needing a separate search engine.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;pg_search&lt;/code&gt;, you get features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relevance Ranking&lt;/strong&gt;: The BM25 algorithm provides an automatic ranking of search results based on how relevant they are to the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy Matching&lt;/strong&gt;: It allows for typo tolerance, meaning searches will return relevant results even when a user makes small errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrase Search&lt;/strong&gt;: You can search for exact phrases or words that are close together, which can be especially useful in content-heavy applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible Queries&lt;/strong&gt;: &lt;code&gt;pg_search&lt;/code&gt; supports more complex search expressions like partial word matches, stemming, and stop-word filtering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Enabling &lt;code&gt;pg_search&lt;/code&gt; on Neon
&lt;/h3&gt;



&lt;p&gt;&lt;code&gt;pg_search&lt;/code&gt; is currently only available on Neon projects created in an &lt;a href="https://neon.tech/docs/introduction/regions#aws-regions" rel="noopener noreferrer"&gt;AWS region&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;Adding &lt;code&gt;pg_search&lt;/code&gt; to your Neon database is simple - just run this single SQL command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_search&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all it takes to enable the extension. You can then use the &lt;code&gt;pg_search&lt;/code&gt; features in your queries. The setup is straightforward, and you don’t need any additional infrastructure to get started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example of Using &lt;code&gt;pg_search&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After enabling the extension, you can create search indexes and run more sophisticated queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create a BM25 index on multiple columns&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;article_search_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;bm25&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Simple keyword search&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;@@@&lt;/span&gt; &lt;span class="s1"&gt;'database'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Handling typos with fuzzy matching&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;@@@&lt;/span&gt; &lt;span class="n"&gt;paradedb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'database'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Sorting by relevance score&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paradedb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;relevance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;@@@&lt;/span&gt; &lt;span class="s1"&gt;'performance'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;paradedb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code shows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a BM25 index that covers multiple columns&lt;/li&gt;
&lt;li&gt;Perform a basic keyword search using the &lt;code&gt;@@@&lt;/code&gt; operator&lt;/li&gt;
&lt;li&gt;Find results even when the search term has typos&lt;/li&gt;
&lt;li&gt;Sort results by relevance so the most relevant results appear first&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Use &lt;code&gt;pg_search&lt;/code&gt; on Neon?
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;pg_search&lt;/code&gt; on Neon gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better search capabilities&lt;/strong&gt;: With ranking, typo tolerance, and complex query options, &lt;code&gt;pg_search&lt;/code&gt; provides much more functionality than &lt;code&gt;tsvector&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fully managed PostgreSQL experience&lt;/strong&gt;: You don’t need to set up or maintain a separate search service. Neon handles everything for you, from scaling to backups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data consistency&lt;/strong&gt;: Since the search index is part of your PostgreSQL database, there’s no need to worry about synchronizing data between separate systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple architecture&lt;/strong&gt;: With &lt;code&gt;pg_search&lt;/code&gt; running on Neon, you avoid the complexity of managing an external search engine while still getting advanced search features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;pg_search&lt;/code&gt; is a great choice for applications that need more advanced search features but want to avoid the complexity of managing a separate search engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  External Search Engines (e.g., Elasticsearch)
&lt;/h2&gt;

&lt;p&gt;External search engines like Elasticsearch provide specialized search features for complex use cases and very large datasets. These engines are designed to scale out across many servers and handle high-performance, low-latency search queries.&lt;/p&gt;

&lt;p&gt;While these engines offer powerful capabilities, they come with trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need to set up and maintain additional infrastructure&lt;/strong&gt;: External search engines require managing separate servers or cloud services, which can increase operational overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You must keep your database and search index synchronized&lt;/strong&gt;: Ensuring that your external search engine stays in sync with your PostgreSQL database can introduce complexity, especially as your data changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increased complexity&lt;/strong&gt;: Managing another system means additional configuration, monitoring, and troubleshooting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher operational costs&lt;/strong&gt;: Running an external search engine comes with extra costs, both in terms of infrastructure and developer time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;External search engines like Elasticsearch provide powerful features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributed search: Handles large-scale search queries across many machines.&lt;/li&gt;
&lt;li&gt;Complex querying: Offers advanced querying capabilities such as aggregations, nested fields, and more.&lt;/li&gt;
&lt;li&gt;Real-time indexing: Updates search results in real time as new data is indexed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Cases for External Search Engines
&lt;/h3&gt;

&lt;p&gt;You might choose an external search engine when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You have extremely large datasets&lt;/strong&gt; (billions of records) that require distributed search across multiple servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your search needs include specialized features&lt;/strong&gt; not available in PostgreSQL, such as advanced analytics, geographic search, or machine learning integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have the resources&lt;/strong&gt; to manage additional infrastructure and complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;tsvector&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;pg_search&lt;/code&gt; on Neon&lt;/th&gt;
&lt;th&gt;External Engines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built into Postgres&lt;/td&gt;
&lt;td&gt;Easy setup on Neon&lt;/td&gt;
&lt;td&gt;Separate system to install, configure and maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Relevance Ranking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;BM25 ranking&lt;/td&gt;
&lt;td&gt;Advanced ranking options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typo Tolerance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Extensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited by Postgres&lt;/td&gt;
&lt;td&gt;Managed by Neon&lt;/td&gt;
&lt;td&gt;Requires manual scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Included with Postgres&lt;/td&gt;
&lt;td&gt;Included with Neon&lt;/td&gt;
&lt;td&gt;Additional infrastructure costs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Handled by Neon&lt;/td&gt;
&lt;td&gt;Requires ongoing maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which Option Should You Choose?
&lt;/h2&gt;

&lt;p&gt;With all these options available, how do you choose the right one for your application? Here are some guidelines:&lt;/p&gt;

&lt;h3&gt;
  
  
  Use built-in &lt;code&gt;tsvector&lt;/code&gt; when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You have simple search requirements&lt;/li&gt;
&lt;li&gt;Your dataset is small to medium-sized&lt;/li&gt;
&lt;li&gt;You want to use only built-in PostgreSQL features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;pg_search&lt;/code&gt; on Neon when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need better search relevance and typo tolerance&lt;/li&gt;
&lt;li&gt;You want to avoid setting up separate systems&lt;/li&gt;
&lt;li&gt;You prefer a managed database experience&lt;/li&gt;
&lt;li&gt;Your search needs are important but don't require specialized features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Consider external search engines when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You have extremely large datasets (billions of records)&lt;/li&gt;
&lt;li&gt;Your search needs include specialized features not available in PostgreSQL&lt;/li&gt;
&lt;li&gt;You have the resources to manage additional infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most web applications, content sites, and e-commerce platforms, &lt;code&gt;pg_search&lt;/code&gt; on Neon provides a great balance between search features and simplicity. It extends PostgreSQL's capabilities without requiring you to manage separate systems or synchronize data.&lt;/p&gt;

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

&lt;p&gt;When choosing a search strategy, start with the simplest option that meets your needs. For many applications, &lt;code&gt;pg_search&lt;/code&gt; on Neon offers a great middle ground - better search features than native PostgreSQL without the complexity of a separate search system.&lt;/p&gt;

&lt;p&gt;Selecting the right search approach allows you to provide good search functionality to your users while keeping your application architecture as simple as possible.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Coding With Cursor and Windsurf Side by Side</title>
      <dc:creator>Sam シ</dc:creator>
      <pubDate>Thu, 27 Mar 2025 14:27:18 +0000</pubDate>
      <link>https://dev.to/neon-postgres/coding-with-cursor-and-windsurf-side-by-side-4gkd</link>
      <guid>https://dev.to/neon-postgres/coding-with-cursor-and-windsurf-side-by-side-4gkd</guid>
      <description>&lt;p&gt;Getting a taste of vibe coding&lt;/p&gt;

&lt;p&gt;Since the release of GitHub Copilot, AI integration in IDEs has evolved dramatically. Now, two IDEs are being talked about more than any other: Windsurf and Cursor. Both tools are forks of VSCode, offer a similar set of features, and use Anthropic’s Claude model under the hood. &lt;/p&gt;

&lt;p&gt;I vibe coded with both IDEs and took some notes, using my experience developing the Neon Python driver as a guiding example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What feels the same?
&lt;/h2&gt;

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

&lt;p&gt;On the surface, Windsurf and Cursor feel strikingly similar. So much so that at first glance, you might think you’re simply looking at two VSCode windows. This means you get the familiar editor you already know and love, with the extensive extension marketplace and setting customization still available. &lt;/p&gt;

&lt;p&gt;Both editors provide the same general interface with the chat window on the right, and the same feature set you’ve come to expect from AI-powered IDEs, like better tab completions, multi file refactoring, agent-based workflows, and context management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tab Completions: Cursor’s Tab vs. Windsurf’s Tabs
&lt;/h2&gt;

&lt;p&gt;I find tab completions one of the most important features of any AI-powered IDE, and it’s where I noticed the biggest productivity gains when developing the Neon Python driver compared to my old setup. &lt;/p&gt;

&lt;p&gt;While both Windsurf and Cursor offer far better autocomplete experiences than GitHub Copilot, Cursor’s tab completions are particularly impressive. Specifically, I’d often make a small change at the top of a file that would need further adjustments below, and Cursor consistently predicted exactly what I needed next—I could just &lt;code&gt;tab-tab-tab&lt;/code&gt; my way through the file. &lt;/p&gt;

&lt;p&gt;For a long time, Cursor held the edge in tab completions when compared to Windsurf’s SuperComplete. But Windsurf is shipping fast. As I was writing this article, they released Windsurf Wave 5, which introduced the Windsurf Tab, and the gap has narrowed. Previously, Cursor felt more aware of the codebase for tabs and provided more useful completions, but Windsurf autocompletions now benefit from the same context engine that powers its AI Agent Cascade, along with a larger and more powerful model.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Context management: Attaching the relevant information
&lt;/h2&gt;

&lt;p&gt;Context management, or the way IDEs handle relevant information to inform their AI suggestions, is another important differentiator. Cursor offers robust support for custom rules that can be conditionally applied based on file type, directory, and other factors. For instance, I easily defined a set of Python rules with strict type hinting, NumPy-style docstrings, and other linting preferences. Throughout my development, Cursor adhered perfectly to these rules, saving me a lot of time in docstring writing and code-style preference refactoring. Windsurf also has built-in rule management, with support for both global rules and rules local to the project, but not for rule file pattern matching or semantic description attachment.&lt;/p&gt;

&lt;p&gt;Yet Windsurf excels in one crucial area: context indexing. Its automatic indexing and understanding of the project structure felt superior. For example, when I made changes to the core logic of the application, Windsurf would reach out to the tests directory and update the relevant unit and integration tests. Cursor is also capable of this, but it requires prompting to get it there, which gets tedious and can easily be forgotten. On the other hand, it offers a wider variety of options for attaching content including most recent changes, git history, lint errors, and most importantly custom docs. Here, at the beginning of my project I was able to create a custom documentation context for Psycopg which I could easily reference. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmoqtstxyclpgrzrzmce.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmoqtstxyclpgrzrzmce.gif" alt="windsurf vs cursor context indexing comparison" width="488" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Windsurf offers only a pre-set documentation library without the means to extend it, though you can attach the link of the documentation as web context to achieve a very similar result. Despite this limitation in customizable context attachments, Windsurf’s automatic context management is really a standout feature and as AI models continue to evolve, this strength will only become more pronounced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent mode: Cursor’s Composer vs Windsurf’s Cascade
&lt;/h2&gt;

&lt;p&gt;Both Windsurf’s Cascade and Cursor’s Composer offer agentic workflows, which allow LLMs to intelligently interact with your codebase, automate repetitive tasks, and implement entire features. &lt;/p&gt;

&lt;p&gt;In practice, I found that both still struggled with more complex coding scenarios. For instance, when implementing Python-to-Postgres type conversions in the Neon driver, both agents had difficulty leveraging Psycopg effectively, because the conversion needed to happen without establishing an actual database connection. Even after I devised a clear implementation plan and explicitly provided it in the prompt instructions, neither IDE could successfully execute without significant manual prompting. &lt;/p&gt;

&lt;p&gt;All hope is not lost, as both agents shined in many other tasks like tests and frontend development. In the case of testing, they generated robust, comprehensive unit and integration tests, accelerating development by at least a few hours without any detailed prompting. For this alone, the agents are well worth having integrated in the IDE. What’s more, to see how each of these fared in more line-heavy development, I tried both out in a React project of mine. After asking for the same component, they each generate working solutions, though Windsurf clearly had a broader understanding of my codebase, utilising some of my custom hooks and API endpoints many directories away. &lt;/p&gt;

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

&lt;p&gt;Both Cursor and Windsurf significantly boosted my productivity when porting the SQL-over-HTTP driver to Python. They both have strengths in different areas —to me, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;strong&gt;Cursor&lt;/strong&gt; excelled in tab completions, and offered tighter, more intuitive control, providing better confidence in the AI coding workflow. &lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Windsurf&lt;/strong&gt; offered better and automatic context indexing, leading to a more “magical” feeling agentic workflow. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This vibe coding experiment not only provided insights into these two IDEs but also unveiled a new way to use Neon. You can now leverage Neon more effectively in Python serverless functions (e.g., AWS Lambdas or Azure Functions) by using SQL over HTTP for efficient single-shot queries and transactions. &lt;/p&gt;

&lt;p&gt;Check out the repository &lt;a href="https://github.com/sam-harri/neon-pyserverless" rel="noopener noreferrer"&gt;here&lt;/a&gt;!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a Multi-Step Form with Laravel Volt, Folio, and Neon Postgres</title>
      <dc:creator>Taraneh Dohmer</dc:creator>
      <pubDate>Fri, 14 Feb 2025 18:36:37 +0000</pubDate>
      <link>https://dev.to/neon-postgres/building-a-multi-step-form-with-laravel-volt-folio-and-neon-postgres-5b4f</link>
      <guid>https://dev.to/neon-postgres/building-a-multi-step-form-with-laravel-volt-folio-and-neon-postgres-5b4f</guid>
      <description>&lt;p&gt;In this guide, we'll walk through the process of building a multi-step form using Laravel &lt;a href="https://livewire.laravel.com/docs/volt" rel="noopener noreferrer"&gt;Volt&lt;/a&gt;, &lt;a href="https://laravel.com/docs/11.x/folio" rel="noopener noreferrer"&gt;Folio&lt;/a&gt;, and Neon Postgres.&lt;/p&gt;

&lt;p&gt;Laravel Volt provides reactivity for dynamic form interactions, Folio offers file-based routing for a clean project structure, and Neon Postgres serves as our scalable database solution.&lt;/p&gt;

&lt;p&gt;Our example app will be a job application form with multiple steps, including personal information, education, and work experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.1 or higher installed&lt;/li&gt;
&lt;li&gt;Composer for managing PHP dependencies&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;Neon&lt;/a&gt; account for Postgres hosting&lt;/li&gt;
&lt;li&gt;Basic familiarity with Laravel and Postgres&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up the Project
&lt;/h2&gt;

&lt;p&gt;Let's start by creating a new Laravel project and setting up the necessary components.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new Laravel project:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   composer create-project laravel/laravel job-application-form
   &lt;span class="nb"&gt;cd &lt;/span&gt;job-application-form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install Laravel Folio for file-based routing:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   composer require laravel/folio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install the Volt Livewire adapter for Laravel, this will also install the Livewire package:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   composer require livewire/volt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;After installing Volt, you can install the Volt service provider:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   php artisan volt:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring the Database Connection
&lt;/h2&gt;

&lt;p&gt;Update your &lt;code&gt;.env&lt;/code&gt; file with your Neon Postgres credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_CONNECTION=pgsql
DB_HOST=your-neon-hostname.neon.tech
DB_PORT=5432
DB_DATABASE=your_database_name
DB_USERNAME=your_username
DB_PASSWORD=your_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your-neon-hostname.neon.tech&lt;/code&gt;, &lt;code&gt;your_database_name&lt;/code&gt;, &lt;code&gt;your_username&lt;/code&gt;, and &lt;code&gt;your_password&lt;/code&gt; with your Neon Postgres connection details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Design
&lt;/h2&gt;

&lt;p&gt;Let's create the database migrations for our job application form. We'll use separate tables for each section and leverage Postgres JSON columns for flexible data storage for additional information.&lt;/p&gt;

&lt;p&gt;First, let's create the migration for the applicants table using the following &lt;code&gt;artisan&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:migration create_applicants_table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;create_applicants_table&lt;/code&gt; migration name follows the Laravel convention of &lt;code&gt;create_{table_name}_table&lt;/code&gt;, where &lt;code&gt;{table_name}&lt;/code&gt; is the name of the table you're creating. That way, Laravel can automatically determine the table name from the migration name, and also it will be easier to identify the purpose of the migration file by its name for other developers.&lt;/p&gt;

&lt;p&gt;This command generates a new migration file in the &lt;code&gt;database/migrations&lt;/code&gt; directory. Open the newly created file and update its content as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Schema\Blueprint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applicants'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'first_name'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'last_name'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'additional_info'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dropIfExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applicants'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This migration creates the &lt;code&gt;applicants&lt;/code&gt; table with fields for &lt;code&gt;first_name&lt;/code&gt;, &lt;code&gt;last_name&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt;. The &lt;code&gt;email&lt;/code&gt; field is set as unique to prevent duplicate applications. We've also included a &lt;code&gt;jsonb&lt;/code&gt; column called &lt;code&gt;additional_info&lt;/code&gt; for storing any extra data that doesn't fit into the predefined columns. This flexibility is one of the advantages of using Postgres with Laravel.&lt;/p&gt;

&lt;p&gt;Next, let's create the migration for the educations table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:migration create_educations_table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the newly created migration file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Schema\Blueprint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'educations'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applicant_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cascade'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'institution'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'degree'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'start_date'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'end_date'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'additional_info'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dropIfExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'educations'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This migration creates the &lt;code&gt;educations&lt;/code&gt; table. It includes a foreign key &lt;code&gt;applicant_id&lt;/code&gt; that references the &lt;code&gt;id&lt;/code&gt; column in the &lt;code&gt;applicants&lt;/code&gt; table. The &lt;code&gt;onDelete('cascade')&lt;/code&gt; ensures that if an applicant is deleted, their education records are also removed. We've included fields for the institution, degree, and start/end dates. Again, we have an &lt;code&gt;additional_info&lt;/code&gt; jsonb column for flexibility.&lt;/p&gt;

&lt;p&gt;Finally, let's create the migration for the work experiences table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:migration create_work_experiences_table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update this migration file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Migrations\Migration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Schema\Blueprint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'work_experiences'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applicant_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cascade'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'company'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'position'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'start_date'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'end_date'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'responsibilities'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'additional_info'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dropIfExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'work_experiences'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This migration creates the &lt;code&gt;work_experiences&lt;/code&gt; table. Similar to the &lt;code&gt;educations&lt;/code&gt; table, it has a foreign key relationship with the &lt;code&gt;applicants&lt;/code&gt; table. It includes fields for the company, position, start/end dates, and responsibilities. The &lt;code&gt;responsibilities&lt;/code&gt; field is of type &lt;code&gt;text&lt;/code&gt; to allow for longer descriptions. We've also included an &lt;code&gt;additional_info&lt;/code&gt; jsonb column here.&lt;/p&gt;

&lt;p&gt;Now that we've created all our migrations, we can run them to create the tables in our database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will execute all the migrations we've just created, setting up the database schema for our job application form.&lt;/p&gt;

&lt;p&gt;One thing to note is that we've used the &lt;code&gt;jsonb&lt;/code&gt; column type for storing additional information in each table. This allows us to store flexible data structures without needing to define a fixed schema. Postgres' JSONB data type is ideal for this use case.&lt;/p&gt;

&lt;p&gt;For your Laravel migrations, you should not use the Neon Postgres Pooler. The Pooler is designed to manage connections for long-running processes, such as web servers, and is not necessary for short-lived processes like migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Models
&lt;/h2&gt;

&lt;p&gt;Next, let's create models for our &lt;code&gt;Applicant&lt;/code&gt;, &lt;code&gt;Education&lt;/code&gt;, and &lt;code&gt;WorkExperience&lt;/code&gt; tables. Models in Laravel are used to interact with database tables and represent the data in your application in an object-oriented way.&lt;/p&gt;

&lt;p&gt;Laravel provides an easy way to generate models using the &lt;code&gt;artisan&lt;/code&gt; command. To create the &lt;code&gt;Applicant&lt;/code&gt; model run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Applicant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates a new file &lt;code&gt;app/Models/Applicant.php&lt;/code&gt;. Open this file and update it with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Factories\HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Applicant&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'first_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'last_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'additional_info'&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$casts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'additional_info'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;educations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Education&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;workExperiences&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WorkExperience&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create the &lt;code&gt;Education&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Education
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the newly created file at &lt;code&gt;app/Models/Education.php&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Factories\HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Education&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'educations'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'applicant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'institution'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'degree'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'start_date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'end_date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'additional_info'&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$casts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'start_date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'end_date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'additional_info'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;applicant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Applicant&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, create the &lt;code&gt;WorkExperience&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model WorkExperience
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the &lt;code&gt;app/Models/WorkExperience.php&lt;/code&gt; file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Factories\HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WorkExperience&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'applicant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'company'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'position'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'start_date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'end_date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'responsibilities'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'additional_info'&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$casts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'start_date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'end_date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'additional_info'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;applicant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Applicant&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's quickly note down the most important parts in these model definitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We've used the &lt;code&gt;$fillable&lt;/code&gt; property to specify which attributes can be mass-assigned. This is a security feature to prevent unintended mass assignment vulnerabilities.&lt;/li&gt;
&lt;li&gt;We've defined relationships between models. An &lt;code&gt;Applicant&lt;/code&gt; has many &lt;code&gt;Education&lt;/code&gt; and &lt;code&gt;WorkExperience&lt;/code&gt; records, while &lt;code&gt;Education&lt;/code&gt; and &lt;code&gt;WorkExperience&lt;/code&gt; belong to an &lt;code&gt;Applicant&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We've used the &lt;code&gt;$casts&lt;/code&gt; property to automatically cast certain attributes to specific types. For example, we're casting the &lt;code&gt;additional_info&lt;/code&gt; field to an array, which works well with Postgres' JSONB column type.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;start_date&lt;/code&gt; and &lt;code&gt;end_date&lt;/code&gt; fields are cast to date objects, which allows for easy date manipulation in PHP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These models will allow us to easily interact with our database tables using Laravel's Eloquent ORM. They provide a convenient way to retrieve, create, update, and delete records, as well as define relationships between different tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a layout for the multi-step form
&lt;/h2&gt;

&lt;p&gt;Before we create the form components, let's set up a layout for our multi-step form. We'll create a main layout file that includes the necessary CSS and JavaScript assets including the Livewire scripts.&lt;/p&gt;

&lt;p&gt;Create a new Blade layout file at &lt;code&gt;resources/views/layouts/app.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="{{ str_replace('_', '-', app()-&amp;gt;getLocale()) }}"&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1"&amp;gt;
    &amp;lt;title&amp;gt;Job Application Form&amp;lt;/title&amp;gt;
    &amp;lt;script src="https://cdn.tailwindcss.com"&amp;gt;&amp;lt;/script&amp;gt;
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    @livewireStyles
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class="container mx-auto mt-8"&amp;gt;
        @yield('content')
    &amp;lt;/div&amp;gt;

    @livewireScripts
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this layout file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We've included the necessary meta tags for character encoding, viewport settings and the page title.&lt;/li&gt;
&lt;li&gt;We've used the &lt;code&gt;@vite&lt;/code&gt; directive to include the CSS and JavaScript assets. This directive is provided by the Laravel Vite package, which integrates Laravel with the Vite build tool for modern frontend development.&lt;/li&gt;
&lt;li&gt;We've included the Livewire styles and scripts. Livewire is a full-stack framework for Laravel that allows you to build dynamic interfaces without writing JavaScript.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To compile the frontend assets, you'll need to run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing File-based Routing with Folio
&lt;/h2&gt;

&lt;p&gt;Laravel Folio was introduced in 2023, and it offers a new approach to routing in Laravel applications.&lt;/p&gt;

&lt;p&gt;It simplifies routing by allowing you to create routes simply by adding Blade templates to a specific directory. This file-based routing system makes your project structure cleaner and more intuitive.&lt;/p&gt;

&lt;p&gt;It is not a replacement for Laravel's built-in routing system but rather a complementary feature that simplifies routing for certain types of applications.&lt;/p&gt;

&lt;p&gt;First, let's set up the directory structure for our multi-step form. Create the following directory structure in your &lt;code&gt;resources/views/pages&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resources/
└── views/
    └── pages/
        ├── index.blade.php
        └── apply/
            ├── index.blade.php
            ├── personal-info.blade.php
            ├── education.blade.php
            ├── work-experience.blade.php
            └── review.blade.php
            └── confirmation.blade.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Folio, each of these Blade files automatically becomes a route. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pages/index.blade.php&lt;/code&gt; will be accessible at the root URL &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pages/apply/personal-info.blade.php&lt;/code&gt; will be accessible at &lt;code&gt;/apply/personal-info&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To create a Folio page, you can use the &lt;code&gt;php artisan folio:page&lt;/code&gt; command. For example, to create a page for the personal information step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan folio:page apply/personal-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above will create a blade file for the in &lt;code&gt;resources/views/pages/apply/personal-info.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div&amp;gt;
    &amp;lt;h2&amp;gt;Personal Information&amp;lt;/h2&amp;gt;
    &amp;lt;!-- Your form content will go here --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can list all available Folio routes using the following Artisan command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan folio:list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can create similar pages for the education, work experience, and review steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan folio:page apply/education
php artisan folio:page apply/work-experience
php artisan folio:page apply/review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will update these files with the form components later in the guide.&lt;/p&gt;

&lt;p&gt;The main thing to remember here is that with Folio, you don't need to manually define routes in a separate routes file. The mere presence of a Blade file in the &lt;code&gt;pages&lt;/code&gt; directory automatically creates a corresponding route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Multi-Step Form with Volt
&lt;/h2&gt;

&lt;p&gt;Volt is a powerful addition to Laravel Livewire that allows you to build reactive components without writing JavaScript. Unlike traditional Livewire components, Volt lets you define your component's state and validation rules directly in the view file, eliminating the need for a separate component class.&lt;/p&gt;

&lt;p&gt;Let's create Volt components for each step of our multi-step form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Personal Information Form
&lt;/h3&gt;

&lt;p&gt;First, create the personal information form component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:volt personal-info-form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That will create a file at &lt;code&gt;resources/views/livewire/personal-info-form.blade.php&lt;/code&gt;. Update the file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

use function Livewire\Volt\state;
use function Livewire\Volt\rules;

state([
    'first_name' =&amp;gt; '',
    'last_name' =&amp;gt; '',
    'email' =&amp;gt; '',
]);

rules([
    'first_name' =&amp;gt; 'required|min:2',
    'last_name' =&amp;gt; 'required|min:2',
    'email' =&amp;gt; 'required|email|unique:applicants,email',
]);

$saveAndContinue = function () {
    $this-&amp;gt;validate();

    $applicant = \App\Models\Applicant::create($this-&amp;gt;only(['first_name', 'last_name', 'email']));
    session(['applicant_id' =&amp;gt; $applicant-&amp;gt;id]);

    return redirect()-&amp;gt;route('apply.education');
};

?&amp;gt;

&amp;lt;div class="max-w-lg p-8 mx-auto bg-white rounded-lg shadow-md"&amp;gt;
    &amp;lt;h2 class="mb-6 text-2xl font-semibold text-gray-800"&amp;gt;Personal Information&amp;lt;/h2&amp;gt;

    &amp;lt;form wire:submit.prevent="saveAndContinue"&amp;gt;
        &amp;lt;!-- First Name --&amp;gt;
        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="first_name" class="block text-sm font-medium text-gray-700"&amp;gt;First Name&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" id="first_name" wire:model="first_name" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('first_name')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="last_name" class="block text-sm font-medium text-gray-700"&amp;gt;Last Name&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" id="last_name" wire:model="last_name" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('last_name')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-6"&amp;gt;
            &amp;lt;label for="email" class="block text-sm font-medium text-gray-700"&amp;gt;Email&amp;lt;/label&amp;gt;
            &amp;lt;input type="email" id="email" wire:model="email" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('email')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div&amp;gt;
            &amp;lt;button type="submit" class="w-full px-4 py-2 text-white transition duration-200 bg-indigo-600 rounded-md hover:bg-indigo-700"&amp;gt;
                Next
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick explanation of the code above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We define the component's state using the &lt;code&gt;state&lt;/code&gt; function, which initializes the form fields.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;rules&lt;/code&gt; function sets up validation rules for each field.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;saveAndContinue&lt;/code&gt; function handles form submission. It validates the form, creates a new &lt;code&gt;Applicant&lt;/code&gt; record, stores the &lt;code&gt;applicant_id&lt;/code&gt; in the session, and redirects to the next step.&lt;/li&gt;
&lt;li&gt;The form fields are bound to the component's state using &lt;code&gt;wire:model&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Validation errors are displayed using &lt;code&gt;@error&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the same way, you can create components for the education, work experience, and review steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Education Form
&lt;/h3&gt;

&lt;p&gt;Next, create the education form component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:volt education-form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;resources/views/livewire/education-form.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

use function Livewire\Volt\state;
use function Livewire\Volt\rules;

state([
    'institution' =&amp;gt; '',
    'degree' =&amp;gt; '',
    'start_date' =&amp;gt; '',
    'end_date' =&amp;gt; '',
]);

rules([
    'institution' =&amp;gt; 'required|min:2',
    'degree' =&amp;gt; 'required|min:2',
    'start_date' =&amp;gt; 'required|date',
    'end_date' =&amp;gt; 'nullable|date|after:start_date',
]);

$saveAndContinue = function () {
    $this-&amp;gt;validate();

    $applicantId = session('applicant_id');
    \App\Models\Education::create(array_merge($this-&amp;gt;all(), ['applicant_id' =&amp;gt; $applicantId]));

    return redirect()-&amp;gt;route('apply.work-experience');
};

?&amp;gt;

&amp;lt;div class="max-w-lg p-8 mx-auto bg-white rounded-lg shadow-md"&amp;gt;
    &amp;lt;h2 class="mb-6 text-2xl font-semibold text-gray-800"&amp;gt;Education&amp;lt;/h2&amp;gt;

    &amp;lt;form wire:submit.prevent="saveAndContinue"&amp;gt;
        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="institution" class="block text-sm font-medium text-gray-700"&amp;gt;Institution&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" id="institution" wire:model="institution" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('institution')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="degree" class="block text-sm font-medium text-gray-700"&amp;gt;Degree&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" id="degree" wire:model="degree" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('degree')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="start_date" class="block text-sm font-medium text-gray-700"&amp;gt;Start Date&amp;lt;/label&amp;gt;
            &amp;lt;input type="date" id="start_date" wire:model="start_date" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('start_date')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-6"&amp;gt;
            &amp;lt;label for="end_date" class="block text-sm font-medium text-gray-700"&amp;gt;End Date&amp;lt;/label&amp;gt;
            &amp;lt;input type="date" id="end_date" wire:model="end_date" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('end_date')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div&amp;gt;
            &amp;lt;button type="submit" class="w-full px-4 py-2 text-white transition duration-200 bg-indigo-600 rounded-md hover:bg-indigo-700"&amp;gt;
                Next
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Work Experience Form
&lt;/h3&gt;

&lt;p&gt;Next, let's create the work experience form component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:volt work-experience-form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;resources/views/livewire/work-experience-form.blade.php&lt;/code&gt; similar to the previous components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

use function Livewire\Volt\state;
use function Livewire\Volt\rules;

state([
    'company' =&amp;gt; '',
    'position' =&amp;gt; '',
    'start_date' =&amp;gt; '',
    'end_date' =&amp;gt; '',
    'responsibilities' =&amp;gt; '',
]);

rules([
    'company' =&amp;gt; 'required|min:2',
    'position' =&amp;gt; 'required|min:2',
    'start_date' =&amp;gt; 'required|date',
    'end_date' =&amp;gt; 'nullable|date|after:start_date',
    'responsibilities' =&amp;gt; 'required|min:10',
]);

$saveAndContinue = function () {
    $this-&amp;gt;validate();

    $applicantId = session('applicant_id');
    \App\Models\WorkExperience::create(array_merge($this-&amp;gt;all(), ['applicant_id' =&amp;gt; $applicantId]));

    return redirect()-&amp;gt;route('apply.review');
};

?&amp;gt;

&amp;lt;div class="max-w-lg p-8 mx-auto bg-white rounded-lg shadow-md"&amp;gt;
    &amp;lt;h2 class="mb-6 text-2xl font-semibold text-gray-800"&amp;gt;Work Experience&amp;lt;/h2&amp;gt;

    &amp;lt;form wire:submit.prevent="saveAndContinue"&amp;gt;
        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="company" class="block text-sm font-medium text-gray-700"&amp;gt;Company&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" id="company" wire:model="company" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('company')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="position" class="block text-sm font-medium text-gray-700"&amp;gt;Position&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" id="position" wire:model="position" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('position')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="start_date" class="block text-sm font-medium text-gray-700"&amp;gt;Start Date&amp;lt;/label&amp;gt;
            &amp;lt;input type="date" id="start_date" wire:model="start_date" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('start_date')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-4"&amp;gt;
            &amp;lt;label for="end_date" class="block text-sm font-medium text-gray-700"&amp;gt;End Date&amp;lt;/label&amp;gt;
            &amp;lt;input type="date" id="end_date" wire:model="end_date" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;
            @error('end_date')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="mb-6"&amp;gt;
            &amp;lt;label for="responsibilities" class="block text-sm font-medium text-gray-700"&amp;gt;Responsibilities&amp;lt;/label&amp;gt;
            &amp;lt;textarea id="responsibilities" wire:model="responsibilities" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"&amp;gt;&amp;lt;/textarea&amp;gt;
            @error('responsibilities')
                &amp;lt;span class="text-sm text-red-500"&amp;gt;{{ $message }}&amp;lt;/span&amp;gt;
            @enderror
        &amp;lt;/div&amp;gt;

        &amp;lt;div&amp;gt;
            &amp;lt;button type="submit" class="w-full px-4 py-2 text-white transition duration-200 bg-indigo-600 rounded-md hover:bg-indigo-700"&amp;gt;
                Next
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Review Form
&lt;/h3&gt;

&lt;p&gt;Finally, create the review form component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:volt review-form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;resources/views/livewire/review-form.blade.php&lt;/code&gt; as we did for the other components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

use function Livewire\Volt\state;
use function Livewire\Volt\{mount};
use App\Models\Applicant;

state(['applicant' =&amp;gt; null]);

mount(function () {
    $applicantId = session('applicant_id');
    $this-&amp;gt;applicant = Applicant::with(['educations', 'workExperiences'])-&amp;gt;findOrFail($applicantId);
});

$submit = function () {
    session()-&amp;gt;flash('message', 'Your application has been submitted successfully!');
    return redirect()-&amp;gt;route('apply.confirmation');
};

?&amp;gt;

&amp;lt;div class="max-w-2xl p-8 mx-auto bg-white rounded-lg shadow-md"&amp;gt;
    &amp;lt;h2 class="mb-6 text-2xl font-semibold text-gray-800"&amp;gt;Review Your Application&amp;lt;/h2&amp;gt;

    &amp;lt;div class="mb-6"&amp;gt;
        &amp;lt;h3 class="mb-3 text-xl font-semibold text-gray-700"&amp;gt;Personal Information&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Name:&amp;lt;/strong&amp;gt; {{ $applicant-&amp;gt;first_name }} {{ $applicant-&amp;gt;last_name }}&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Email:&amp;lt;/strong&amp;gt; {{ $applicant-&amp;gt;email }}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="mb-6"&amp;gt;
        &amp;lt;h3 class="mb-3 text-xl font-semibold text-gray-700"&amp;gt;Education&amp;lt;/h3&amp;gt;
        @foreach($applicant-&amp;gt;educations as $education)
            &amp;lt;div class="p-4 mb-4 rounded-lg shadow-sm bg-gray-50"&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Institution:&amp;lt;/strong&amp;gt; {{ $education-&amp;gt;institution }}&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Degree:&amp;lt;/strong&amp;gt; {{ $education-&amp;gt;degree }}&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Period:&amp;lt;/strong&amp;gt; {{ $education-&amp;gt;start_date }} - {{ $education-&amp;gt;end_date ?: 'Present' }}&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        @endforeach
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="mb-6"&amp;gt;
        &amp;lt;h3 class="mb-3 text-xl font-semibold text-gray-700"&amp;gt;Work Experience&amp;lt;/h3&amp;gt;
        @foreach($applicant-&amp;gt;workExperiences as $experience)
            &amp;lt;div class="p-4 mb-4 rounded-lg shadow-sm bg-gray-50"&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Company:&amp;lt;/strong&amp;gt; {{ $experience-&amp;gt;company }}&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Position:&amp;lt;/strong&amp;gt; {{ $experience-&amp;gt;position }}&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Period:&amp;lt;/strong&amp;gt; {{ $experience-&amp;gt;start_date }} - {{ $experience-&amp;gt;end_date ?: 'Present' }}&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Responsibilities:&amp;lt;/strong&amp;gt; {{ $experience-&amp;gt;responsibilities }}&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        @endforeach
    &amp;lt;/div&amp;gt;

    &amp;lt;div&amp;gt;
        &amp;lt;button wire:click="submit" class="w-full px-4 py-2 text-white transition duration-200 bg-indigo-600 rounded-md hover:bg-indigo-700"&amp;gt;
            Submit Application
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These Volt components handle the state management, validation, and submission logic for each step of the multi-step form. That way Volt simplifies the process of creating interactive components by allowing you to define both the logic and the template in a single file.&lt;/p&gt;

&lt;p&gt;To use these components in your Folio pages and make the routes named, you can include them like this. Named routes allow you to easily reference routes by name throughout your application. We also need to extend a layout for each page to ensure a consistent structure.&lt;/p&gt;

&lt;p&gt;First, in each file, you will define a named route using the &lt;code&gt;name&lt;/code&gt; function and extend the layout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the &lt;code&gt;resources/views/pages/apply/personal-info.blade.php&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;?php
  use function Laravel\Folio\name;

  name('apply.personal-info');
  ?&amp;gt;

  @extends('layouts.app')

  @section('title', 'Personal Information')

  @section('content')
  &amp;lt;div&amp;gt;
      &amp;lt;livewire:personal-info-form /&amp;gt;
  &amp;lt;/div&amp;gt;
  @endsection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to do the same for the other pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the &lt;code&gt;resources/views/pages/apply/education.blade.php&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;?php
  use function Laravel\Folio\name;

  name('apply.education');
  ?&amp;gt;

  @extends('layouts.app')

  @section('title', 'Education')

  @section('content')
  &amp;lt;div&amp;gt;
      &amp;lt;livewire:education-form /&amp;gt;
  &amp;lt;/div&amp;gt;
  @endsection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For the &lt;code&gt;resources/views/pages/apply/work-experience.blade.php&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;?php
  use function Laravel\Folio\name;

  name('apply.work-experience');
  ?&amp;gt;

  @extends('layouts.app')

  @section('title', 'Work Experience')

  @section('content')
  &amp;lt;div&amp;gt;
      &amp;lt;livewire:work-experience-form /&amp;gt;
  &amp;lt;/div&amp;gt;
  @endsection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;And for the &lt;code&gt;resources/views/pages/apply/review.blade.php&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;?php
  use function Laravel\Folio\name;

  name('apply.review');
  ?&amp;gt;

  @extends('layouts.app')

  @section('title', 'Review')

  @section('content')
  &amp;lt;div&amp;gt;
      &amp;lt;livewire:review-form /&amp;gt;
  &amp;lt;/div&amp;gt;
  @endsection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Confirmation Page
&lt;/h3&gt;

&lt;p&gt;Finally, create a confirmation page for the application submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan folio:page apply/confirmation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;resources/views/pages/apply/confirmation.blade.php&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
use function Laravel\Folio\name;

name('apply.confirmation');
?&amp;gt;

@extends('layouts.app')

@section('title', 'Confirmation')

@section('content')
&amp;lt;div class="max-w-md p-8 mx-auto text-center bg-white rounded-lg shadow-md"&amp;gt;
    &amp;lt;h2 class="mb-4 text-2xl font-semibold text-green-600"&amp;gt;Application Submitted&amp;lt;/h2&amp;gt;

    &amp;lt;p class="mb-6 text-gray-700"&amp;gt;{{ session('message') }}&amp;lt;/p&amp;gt;

    &amp;lt;a href="/" class="inline-block px-4 py-2 text-white transition duration-200 bg-indigo-600 rounded-md hover:bg-indigo-700"&amp;gt;
        Return to Homepage
    &amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;
@endsection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This page displays a success message after the application is submitted and provides a link to return to the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Multi-Step Form
&lt;/h2&gt;

&lt;p&gt;To manually verify that everything works as expected, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you haven't already, start the Laravel development server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   php artisan serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open your browser and navigate to &lt;code&gt;http://localhost:8000/apply/personal-info&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill out the personal information form and submit it. You should be redirected to the education form.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill out the education form and submit it. You should be redirected to the work experience form.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill out the work experience form and submit it. You should be redirected to the review page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the review page, verify that all the information you entered is displayed correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Submit the application and verify that you see a success message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To check if the data was persisted correctly:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Open a database client (like pgAdmin for Postgres) and connect to your Neon database.&lt;/li&gt;
&lt;li&gt;Check the &lt;code&gt;applicants&lt;/code&gt;, &lt;code&gt;educations&lt;/code&gt;, and &lt;code&gt;work_experiences&lt;/code&gt; tables. You should see your submitted data.&lt;/li&gt;
&lt;li&gt;Verify that the &lt;code&gt;applicant_id&lt;/code&gt; in the &lt;code&gt;educations&lt;/code&gt; and &lt;code&gt;work_experiences&lt;/code&gt; tables matches the &lt;code&gt;id&lt;/code&gt; in the &lt;code&gt;applicants&lt;/code&gt; table for your submission.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Try refreshing the page or closing and reopening your browser, then navigate back to &lt;code&gt;http://localhost:8000/apply/review&lt;/code&gt;. You should still see your submitted data, demonstrating that the data persists across sessions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Besides manual testing, you can also write automated tests to make sure your multi-step form works correctly. Laravel provides a testing suite that allows you to write unit, feature, and browser tests.&lt;/p&gt;

&lt;p&gt;Create feature tests for your multi-step form to ensure each step works correctly. Here's an example for the personal info step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Tests\Feature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Tests\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Applicant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Livewire\Volt\Volt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Testing\RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PersonalInfoTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_can_submit_personal_info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Volt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'personal-info-form'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'first_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'last_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Doe'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'john@example.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'saveAndContinue'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/apply/education'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applicants'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'first_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'last_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Doe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'john@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applicant_id'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test checks if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The form can be submitted with valid data.&lt;/li&gt;
&lt;li&gt;The data is correctly stored in the database.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;applicant_id&lt;/code&gt; is stored in the session.&lt;/li&gt;
&lt;li&gt;The user is redirected to the next step after submission.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can create similar tests for the education and work experience steps.&lt;/p&gt;

&lt;p&gt;To learn more about testing in Laravel, check out the &lt;a href="https://neon.tech/guides/laravel-test-on-branch" rel="noopener noreferrer"&gt;Testing Laravel Applications with Neon's Database Branching&lt;/a&gt; guide.&lt;/p&gt;

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

&lt;p&gt;In this guide, we've built a multi-step form using Laravel Volt, Folio, and Neon Postgres. We've covered form validation, data storage, and routing, demonstrating how these tools can be used together to create a dynamic and interactive form.&lt;/p&gt;

&lt;p&gt;To further improve this project, consider adding features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File uploads for resumes&lt;/li&gt;
&lt;li&gt;Email notifications to applicants&lt;/li&gt;
&lt;li&gt;An admin interface to review applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to keep in mind is always to validate and sanitize user inputs, optimize your database queries, and thoroughly test your application before deploying to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs" rel="noopener noreferrer"&gt;Laravel Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.tech/docs" rel="noopener noreferrer"&gt;Neon Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.tech/docs/guides/branching-github-actions" rel="noopener noreferrer"&gt;Neon Branching GitHub Actions Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



</description>
    </item>
    <item>
      <title>Building a RESTful API with ASP.NET Core, Swagger, and Neon</title>
      <dc:creator>Rishi Raj Jain</dc:creator>
      <pubDate>Thu, 23 Jan 2025 19:19:14 +0000</pubDate>
      <link>https://dev.to/neon-postgres/building-a-restful-api-with-aspnet-core-swagger-and-neon-9el</link>
      <guid>https://dev.to/neon-postgres/building-a-restful-api-with-aspnet-core-swagger-and-neon-9el</guid>
      <description>&lt;p&gt;In this guide, we'll walk through the process of developing a RESTful API using ASP.NET Core, connecting it to a Neon Postgres database. We will cover CRUD operations using Entity Framework Core (EF Core), generate interactive API documentation with Swagger, and explore best practices for testing your API endpoints. As a bonus, we'll also implement JWT authentication to secure your endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dotnet.microsoft.com/download/dotnet/8.0" rel="noopener noreferrer"&gt;.NET SDK 8.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;Neon account&lt;/a&gt; for setting up your Postgres database&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.postman.com/downloads/" rel="noopener noreferrer"&gt;Postman&lt;/a&gt; for API testing&lt;/li&gt;
&lt;li&gt;Basic knowledge of C# and ASP.NET Core&lt;/li&gt;
&lt;li&gt;Familiarity with Entity Framework Core&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting Up Your ASP.NET Core Project
&lt;/h2&gt;

&lt;p&gt;First, create a new ASP.NET Core Web API project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new webapi &lt;span class="nt"&gt;-n&lt;/span&gt; NeonApi
&lt;span class="nb"&gt;cd &lt;/span&gt;NeonApi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the required NuGet packages using the &lt;code&gt;dotnet add package&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Swashbuckle.AspNetCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.EntityFrameworkCore.Design
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above packages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.EntityFrameworkCore&lt;/code&gt; - Entity Framework Core for database operations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Npgsql.EntityFrameworkCore.PostgreSQL&lt;/code&gt; - PostgreSQL provider for EF Core&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Swashbuckle.AspNetCore&lt;/code&gt; - Swagger for API documentation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.AspNetCore.Authentication.JwtBearer&lt;/code&gt; - JWT authentication for securing endpoints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.EntityFrameworkCore.Design&lt;/code&gt; - EF Core design tools for migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuring the Neon Database
&lt;/h3&gt;

&lt;p&gt;Head over to your &lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;Neon Dashboard&lt;/a&gt; and create a new project.&lt;/p&gt;

&lt;p&gt;Once done, grab your database connection string and add it to your &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"NeonDb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Host=&amp;lt;your-host&amp;gt;;Database=&amp;lt;your-database&amp;gt;;Username=&amp;lt;your-username&amp;gt;;Password=&amp;lt;your-password&amp;gt;;Port=5432"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you're in &lt;code&gt;appsettings.json&lt;/code&gt;, add a section for JWT authentication after the connection string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Jwt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"SecretKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-very-secure-secret-key"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will cover JWT authentication in more detail later in this guide, but for now, let's focus on setting up the API.&lt;/p&gt;

&lt;p&gt;Next, update your &lt;code&gt;Program.cs&lt;/code&gt; file to include the database context, Swagger, and JWT authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NeonApi.Data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Authentication.JwtBearer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.IdentityModel.Tokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Add the Neon database context using Npgsql provider&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NeonDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNpgsql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NeonDb"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="c1"&gt;// Register controllers for handling incoming HTTP requests&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Enable API endpoint exploration and Swagger documentation&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEndpointsApiExplorer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSwaggerGen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Set up JWT authentication to secure API endpoints&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJwtBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Validate that the token is signed with the specified key&lt;/span&gt;
            &lt;span class="n"&gt;ValidateIssuerSigningKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IssuerSigningKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SymmetricSecurityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:SecretKey"&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt;

            &lt;span class="c1"&gt;// Disable issuer and audience validation for testing purposes&lt;/span&gt;
            &lt;span class="n"&gt;ValidateIssuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ValidateAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Configure the middleware pipeline&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwagger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwaggerUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttpsRedirection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthentication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapControllers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above, we configure the necessary services directly within &lt;code&gt;Program.cs&lt;/code&gt; to connect our ASP.NET Core API to Neon and secure it with JWT authentication:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We use &lt;code&gt;AddDbContext&lt;/code&gt; to set up &lt;code&gt;NeonDbContext&lt;/code&gt; with the Npgsql provider, connecting to the Neon database using the connection string defined in &lt;code&gt;appsettings.json&lt;/code&gt;. Make sure to update &lt;code&gt;"NeonDb"&lt;/code&gt; with your actual connection string key if it's named differently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We register controllers with &lt;code&gt;AddControllers()&lt;/code&gt;, which allows the application to handle incoming API requests and map them to their respective endpoints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By adding &lt;code&gt;EndpointsApiExplorer&lt;/code&gt; and &lt;code&gt;SwaggerGen&lt;/code&gt;, we enable automatic generation of API documentation. This provides a user-friendly interface to interact with your API endpoints, accessible at &lt;code&gt;/swagger&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For the JWT authentication setup, we are implementing the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Here, we set up JWT authentication to protect your API routes. We use &lt;code&gt;AddJwtBearer&lt;/code&gt; to validate tokens sent by clients.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;TokenValidationParameters&lt;/code&gt; section allows us to make sure that the token is signed with the specified key. Replace &lt;code&gt;"your-secret-key"&lt;/code&gt; in &lt;code&gt;appsettings.json&lt;/code&gt; with a secure key unique to your application.&lt;/li&gt;
&lt;li&gt;For simplicity, &lt;code&gt;ValidateIssuer&lt;/code&gt; and &lt;code&gt;ValidateAudience&lt;/code&gt; are set to &lt;code&gt;false&lt;/code&gt;, which means the API won't check who issued the token or its intended audience. This is useful for local development but should be tightened for production environments.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To avoid hardcoding sensitive information like the secret key, consider using environment variables or a configuration management system to securely store secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Entity Framework Core Models
&lt;/h2&gt;

&lt;p&gt;Data models define the structure of your database tables and the relationships between them. Here, we'll create a simple &lt;code&gt;Product&lt;/code&gt; model to represent products in our Neon database.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;Models&lt;/code&gt; folder, create a &lt;code&gt;Product.cs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;NeonApi.Models&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we define a simple &lt;code&gt;Product&lt;/code&gt; model with four properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Id&lt;/code&gt;: This is the primary key, which will auto-increment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Name&lt;/code&gt;: Stores the product's name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Price&lt;/code&gt;: Holds the product's price as a decimal value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Description&lt;/code&gt;: Provides additional details about the product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each property corresponds to a column in the database table that Entity Framework will generate for us.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Database Context
&lt;/h3&gt;

&lt;p&gt;Next, we need to create a database context class, which serves as a bridge between our C# code and the Neon database.&lt;/p&gt;

&lt;p&gt;Create a new folder named &lt;code&gt;Data&lt;/code&gt; and add a &lt;code&gt;NeonDbContext.cs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;NeonApi.Data&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NeonDbContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;NeonDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NeonDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// This DbSet represents the Products table in the Neon database&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Products&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code snippet does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;NeonDbContext&lt;/code&gt; class inherits from &lt;code&gt;DbContext&lt;/code&gt;, which is part of Entity Framework Core.&lt;/li&gt;
&lt;li&gt;We pass &lt;code&gt;DbContextOptions&lt;/code&gt; to the constructor to configure the connection to our Neon database.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;DbSet&amp;lt;Product&amp;gt;&lt;/code&gt; property represents the &lt;code&gt;Products&lt;/code&gt; table. This allows us to perform CRUD operations on the &lt;code&gt;Product&lt;/code&gt; model directly through this context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Running Migrations to Create the Database Schema
&lt;/h3&gt;

&lt;p&gt;Now that we have defined our model and context, let's generate the database schema using migrations. Open your terminal and run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet ef migrations add InitialCreate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above command generates a migration file based on the changes made to the database schema. The migration file contains instructions to create the &lt;code&gt;Products&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Next, apply the migration to your Neon database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet ef database update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dotnet ef database update&lt;/code&gt; command applies the migration to your Neon database, creating the &lt;code&gt;Products&lt;/code&gt; table and any other necessary schema changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Make sure your database connection string in &lt;code&gt;appsettings.json&lt;/code&gt; is correctly configured before running the migrations. That way the changes are applied to your Neon database instance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this point, your database is set up and ready to store product data!&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the API Endpoints
&lt;/h2&gt;

&lt;p&gt;With the database schema in place, let's create the API endpoints to perform CRUD operations on the &lt;code&gt;Products&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;Controllers&lt;/code&gt; folder, create a &lt;code&gt;ProductsController.cs&lt;/code&gt; file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Mvc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NeonApi.Data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NeonApi.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/[controller]"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;NeonDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ProductsController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NeonDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Retrieve all products from the database&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Retrieve a single product by ID&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Return 404 if not found&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Add a new product to the database&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Return 201 Created status with the newly created product&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;CreatedAtAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Ensure the ID in the URL matches the ID of the provided product&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Mark the product as modified&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Modified&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Return 204 No Content status after a successful update&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DeleteProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Find the product by ID&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Return 404 if not found&lt;/span&gt;

        &lt;span class="c1"&gt;// Remove the product from the database&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Return 204 No Content status after successful deletion&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code above, we define a &lt;code&gt;ProductsController&lt;/code&gt; to handle all CRUD operations for our &lt;code&gt;Product&lt;/code&gt; model. Here's a breakdown of how each endpoint works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;GetProducts&lt;/code&gt; method handles &lt;code&gt;GET /api/products&lt;/code&gt; requests, fetching all products stored in the Neon database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;GetProduct&lt;/code&gt; method handles &lt;code&gt;GET /api/products/{id}&lt;/code&gt; requests to retrieve a single product by its unique ID. If no product with the given ID is found, it responds with a &lt;code&gt;404 Not Found&lt;/code&gt;. This ensures the client is notified when attempting to access a non-existent product.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;CreateProduct&lt;/code&gt; method handles &lt;code&gt;POST /api/products&lt;/code&gt; requests to add a new product. The response uses &lt;code&gt;CreatedAtAction&lt;/code&gt; to include a link to the newly created resource, following REST best practices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;UpdateProduct&lt;/code&gt; method handles &lt;code&gt;PUT /api/products/{id}&lt;/code&gt; requests to modify an existing product. Once the update is successful, it responds with a &lt;code&gt;204 No Content&lt;/code&gt;, indicating the operation was successful without returning any additional data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;DeleteProduct&lt;/code&gt; method handles &lt;code&gt;DELETE /api/products/{id}&lt;/code&gt; requests to remove a product by its ID. If the product doesn't exist, it returns a &lt;code&gt;404 Not Found&lt;/code&gt; response.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each endpoint is fully asynchronous and interacts with the Neon database through the &lt;code&gt;NeonDbContext&lt;/code&gt; context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Swagger for API Documentation
&lt;/h2&gt;

&lt;p&gt;To document our API and provide an interactive interface for testing, we'll integrate Swagger into our ASP.NET Core project.&lt;/p&gt;

&lt;p&gt;Swagger automatically generates OpenAPI documentation, making it easy to explore your API and test its endpoints directly from your browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling Swagger in &lt;code&gt;Program.cs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To set up Swagger, add the following code in the &lt;code&gt;Configure&lt;/code&gt; method of your &lt;code&gt;Program.cs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwagger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwaggerUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Configure Swagger UI at the root URL&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwaggerEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/swagger/v1/swagger.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Neon API V1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoutePrefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;UseSwagger&lt;/code&gt; middleware generates the OpenAPI documentation for your API, while &lt;code&gt;UseSwaggerUI&lt;/code&gt; sets up the Swagger interface for interacting with your endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Your Application
&lt;/h3&gt;

&lt;p&gt;Now that Swagger is set up, start your application using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the application is running, you will see the port where your API is hosted (usually &lt;code&gt;https://localhost:5229&lt;/code&gt;), eg.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5229
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;https://localhost:5229/swagger&lt;/code&gt; in your browser to access the Swagger UI.&lt;/p&gt;

&lt;p&gt;The Swagger UI will appear, displaying all your API endpoints with detailed documentation. You can test the endpoints by sending requests directly from the UI and viewing the responses, making it easy to verify that everything works as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your API with Postman
&lt;/h2&gt;

&lt;p&gt;Now that your API is running, you can use Postman or any other API testing tool to interact with your endpoints. You can even use the Swagger UI to test the endpoints, but Postman provides a more robust environment for testing complex scenarios.&lt;/p&gt;

&lt;p&gt;In this section, we'll walk through testing the CRUD operations using Postman but feel free to use any tool you're comfortable with like Insomnia or cURL.&lt;/p&gt;

&lt;p&gt;Download and launch &lt;a href="https://www.postman.com/downloads/" rel="noopener noreferrer"&gt;Postman&lt;/a&gt;. Create a new request to interact with your API.&lt;/p&gt;

&lt;p&gt;Open Postman and create the following requests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET&lt;/code&gt;: &lt;code&gt;/api/products&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: Fetches all products.&lt;/li&gt;
&lt;li&gt;Set to &lt;code&gt;GET&lt;/code&gt;, enter &lt;code&gt;https://localhost:5001/api/products&lt;/code&gt;, and click &lt;strong&gt;Send&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You should receive a &lt;code&gt;200 OK&lt;/code&gt; response with a list of products.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST&lt;/code&gt;: &lt;code&gt;/api/products&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: Creates a new product.&lt;/li&gt;
&lt;li&gt;Set to &lt;code&gt;POST&lt;/code&gt;, enter &lt;code&gt;https://localhost:5001/api/products&lt;/code&gt;, and go to &lt;strong&gt;Body&lt;/strong&gt; → &lt;strong&gt;raw&lt;/strong&gt; → &lt;strong&gt;JSON&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New Product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sample product"&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Send&lt;/strong&gt;. Expect a &lt;code&gt;201 Created&lt;/code&gt; response.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PUT&lt;/code&gt;: &lt;code&gt;/api/products/{id}&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: Updates a product.&lt;/li&gt;
&lt;li&gt;Set to &lt;code&gt;PUT&lt;/code&gt;, enter &lt;code&gt;https://localhost:5001/api/products/1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Updated Product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Updated description"&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Send&lt;/strong&gt;. You should receive a &lt;code&gt;204 No Content&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;DELETE&lt;/code&gt;: &lt;code&gt;/api/products/{id}&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: Deletes a product.&lt;/li&gt;
&lt;li&gt;Set to &lt;code&gt;DELETE&lt;/code&gt;, enter &lt;code&gt;https://localhost:5001/api/products/1&lt;/code&gt;, and click &lt;strong&gt;Send&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Expect a &lt;code&gt;204 No Content&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After testing, check that all changes are reflected in your Neon database. Use both Postman and Swagger UI to confirm the endpoints are functioning correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Your API with JWT Authentication (Bonus)
&lt;/h2&gt;

&lt;p&gt;To protect your API endpoints, we’ll use &lt;a href="https://jwt.io/" rel="noopener noreferrer"&gt;JWT&lt;/a&gt; (JSON Web Token) authentication. By adding the &lt;code&gt;[Authorize]&lt;/code&gt; attribute to specific controller actions, you can ensure that only authenticated users have access. Here’s how to secure the &lt;code&gt;GetProducts&lt;/code&gt; endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, any requests to &lt;code&gt;GetProducts&lt;/code&gt; will require a valid JWT token, if you try to access the endpoint without a token, you will receive a &lt;code&gt;401 Unauthorized&lt;/code&gt; response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating and Using JWT Tokens
&lt;/h3&gt;

&lt;p&gt;When a user successfully logs in, the server generates a JWT token containing the user's authentication details. This token typically includes claims such as the user's ID, email, and a unique identifier. The token is then signed with a secret key to ensure its integrity and prevent tampering. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GenerateJwtToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtRegisteredClaimNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtRegisteredClaimNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtRegisteredClaimNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Jti&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SymmetricSecurityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:SecretKey"&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SigningCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SecurityAlgorithms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HmacSha256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JwtSecurityToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;signingCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JwtSecurityTokenHandler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;WriteToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImp0aSI6IjMzYjQwMzIwLWUwZjItNDIwZi1iZjIwLWUwZjIwZmJmMjAwMCIsImV4cCI6MTY0MzUwNzQwMH0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you were to go to &lt;a href="https://jwt.io/" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt;, you could paste the token and see its decoded contents.&lt;/p&gt;

&lt;p&gt;Once the token is generated, the client stores it in local storage or session storage and includes it in the &lt;code&gt;Authorization&lt;/code&gt; header for all subsequent requests to secured endpoints. For instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: Bearer &amp;lt;your-jwt-token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this header in place, the server can authenticate the user without requiring them to log in again for each request.&lt;/p&gt;

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

&lt;p&gt;In this guide, we covered the process of building a RESTful API with ASP.NET Core, connecting it to a Neon Postgres database, and securing it with JWT authentication. We explored CRUD operations using Entity Framework Core, generated interactive API documentation with Swagger, and tested our endpoints using Postman.&lt;/p&gt;

&lt;p&gt;As a next step, consider expanding your API with additional features, such as pagination, filtering, or sorting. You can also explore adding testing frameworks like xUnit or NUnit to write unit tests for your API endpoints.&lt;/p&gt;

&lt;p&gt;For more information, check out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://neon.tech/docs/introduction" rel="noopener noreferrer"&gt;Neon Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npgsql.org/doc/index.html" rel="noopener noreferrer"&gt;Npgsql Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/ef/core/" rel="noopener noreferrer"&gt;Entity Framework Core Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



</description>
      <category>webdev</category>
      <category>postgressql</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Creating a Multi-Tenant Application with Laravel and Neon</title>
      <dc:creator>Rishi Raj Jain</dc:creator>
      <pubDate>Fri, 10 Jan 2025 19:27:38 +0000</pubDate>
      <link>https://dev.to/neon-postgres/creating-a-multi-tenant-application-with-laravel-and-neon-2kp2</link>
      <guid>https://dev.to/neon-postgres/creating-a-multi-tenant-application-with-laravel-and-neon-2kp2</guid>
      <description>&lt;p&gt;Multi-tenancy is a software architecture where a single instance of an application serves multiple tenants or clients.&lt;/p&gt;

&lt;p&gt;Each tenant's data is isolated and remains invisible to other tenants. This approach is commonly used in Software as a Service (SaaS) applications. In this tutorial, we'll build the foundation for a multi-tenant SaaS application using Laravel and Neon.&lt;/p&gt;

&lt;p&gt;By the end of this tutorial, you'll have a fully functional multi-tenant SaaS application where tenants can manage their own books, users, and settings, all while maintaining data isolation between tenants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.1 or higher installed on your system&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; for managing PHP dependencies&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; and npm for managing front-end assets&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://console.neon.tech/signup" rel="noopener noreferrer"&gt;Neon&lt;/a&gt; account for database hosting&lt;/li&gt;
&lt;li&gt;Basic knowledge of Laravel and Livewire&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up the Project
&lt;/h2&gt;

&lt;p&gt;Let's start by creating a new Laravel project and setting up the necessary components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a New Laravel Project
&lt;/h3&gt;

&lt;p&gt;Open your terminal and run the following command to create a new Laravel project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer create-project laravel/laravel laravel-multi-tenant-saas
&lt;span class="nb"&gt;cd &lt;/span&gt;laravel-multi-tenant-saas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Required Packages
&lt;/h3&gt;

&lt;p&gt;For our multi-tenant SaaS application, we'll use the following package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stancl/tenancy&lt;/code&gt;: A flexible multi-tenancy package for Laravel&lt;/li&gt;
&lt;li&gt;Laravel Breeze: A minimal authentication starter kit for Laravel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start by installing the &lt;code&gt;stancl/tenancy&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require stancl/tenancy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the package, let's set up the tenancy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan tenancy:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the &lt;code&gt;TenancyServiceProvider&lt;/code&gt; in the &lt;code&gt;bootstrap/providers.php&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nc"&gt;App\Providers\TenancyServiceProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's install Laravel Breeze with the Blade views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/breeze &lt;span class="nt"&gt;--dev&lt;/span&gt;
php artisan breeze:install blade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install the required NPM packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting up the Database
&lt;/h3&gt;

&lt;p&gt;Update your &lt;code&gt;.env&lt;/code&gt; file with your Neon database credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_CONNECTION=pgsql
DB_HOST=your-neon-hostname.neon.tech
DB_PORT=5432
DB_DATABASE=your_database_name
DB_USERNAME=your_username
DB_PASSWORD=your_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After updating the &lt;code&gt;.env&lt;/code&gt; file, run the database migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing Multi-Tenancy
&lt;/h2&gt;

&lt;p&gt;Now that we have our basic setup, let's implement multi-tenancy in our application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Tenant Model
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;Tenant&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;app/Models/Tenant.php&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Factories\HasFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stancl\Tenancy\Database\Models\Tenant&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;BaseTenant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stancl\Tenancy\Contracts\TenantWithDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stancl\Tenancy\Database\Concerns\HasDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stancl\Tenancy\Database\Concerns\HasDomains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tenant&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseTenant&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;TenantWithDatabase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HasDatabase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HasDomains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This model extends the base &lt;code&gt;Tenant&lt;/code&gt; model provided by the tenancy package and implements the &lt;code&gt;TenantWithDatabase&lt;/code&gt; interface. We've also defined the fillable attributes and custom columns for our tenant.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HasDatabase&lt;/code&gt; and &lt;code&gt;HasDomains&lt;/code&gt; traits provided by the tenancy package allow us to manage tenant-specific databases and domains. This essentially means that each tenant will have its own database and domain providing data isolation between tenants.&lt;/p&gt;

&lt;p&gt;To learn more about the tenancy package event system and how to customize the tenant model, refer to the &lt;a href="https://tenancyforlaravel.com/docs/v3/event-system" rel="noopener noreferrer"&gt;stancl/tenancy documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Tenancy
&lt;/h3&gt;

&lt;p&gt;Update the &lt;code&gt;config/tenancy.php&lt;/code&gt; file to use our custom &lt;code&gt;Tenant&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'tenant_model'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Models\Tenant&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, update the central domains configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'central_domains'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'laravel-multi-tenant-saas.test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the default central domains with your own domain names.&lt;/p&gt;

&lt;p&gt;This is an important part as this is how the tenancy package will determine which domain belongs to which tenant and load the tenant-specific data accordingly.&lt;/p&gt;

&lt;p&gt;Feel free to review the other configuration options in the &lt;code&gt;config/tenancy.php&lt;/code&gt; file to customize the tenancy behavior based on your requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Tenant Migrations
&lt;/h3&gt;

&lt;p&gt;The tenancy package has built-in event listeners that automatically run tenant-specific migrations when a tenant is created. For this we need to make sure that all of the tenant-specific migrations are in the &lt;code&gt;database/migrations/tenant&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;As each tenant will have its own database, the migrations in the tenant directory will be used to create tenant-specific tables in the tenant's database.&lt;/p&gt;

&lt;p&gt;Start by copying the default User migration to the &lt;code&gt;database/migrations/tenant&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;database/migrations/0001_01_01_000000_create_users_table.php database/migrations/tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will be the base migration for tenant-specific tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Tenant Routes
&lt;/h3&gt;

&lt;p&gt;The tenancy package provides middleware to handle tenant-specific routes. This allows you to define routes that are accessible only to tenants and not to central domains.&lt;/p&gt;

&lt;p&gt;Start by creating a new file &lt;code&gt;routes/tenant.php&lt;/code&gt; for tenant-specific routes with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stancl\Tenancy\Middleware\InitializeTenancyByDomain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/*
|--------------------------------------------------------------------------
| Tenant Routes
|--------------------------------------------------------------------------
|
| Here you can register the tenant routes for your application.
| These routes are loaded by the TenantRouteServiceProvider.
|
| Feel free to customize them however you want. Good luck!
|
*/&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'web'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;InitializeTenancyByDomain&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;PreventAccessFromCentralDomains&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'This is your multi-tenant application. The id of the current tenant is '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Here you can add more tenant-specific routes&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These routes will be loaded by the &lt;code&gt;TenantRouteServiceProvider&lt;/code&gt; and will be accessible only to tenants. The &lt;code&gt;InitializeTenancyByDomain&lt;/code&gt; middleware will set the current tenant based on the domain, and the &lt;code&gt;PreventAccessFromCentralDomains&lt;/code&gt; middleware will prevent access from central domains.&lt;/p&gt;

&lt;p&gt;For more information on how to customize the tenancy routes, refer to the &lt;a href="https://tenancyforlaravel.com/docs/v3/routes" rel="noopener noreferrer"&gt;stancl/tenancy documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Tenant Creation
&lt;/h3&gt;

&lt;p&gt;Create a controller for tenant registration, this would usually be done by the admin users of the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:controller TenantController
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;app/Http/Controllers/TenantController.php&lt;/code&gt; controller and implement the tenant registration process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Tenant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;showRegistrationForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.register'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'domain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|max:255|unique:domains,domain'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tenant&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$tenant&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'domain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.registered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;registered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.registered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'domain'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This controller handles tenant registration, creates a new tenant in the database, and sets up the tenant's domain. The &lt;code&gt;TenancyServiceProvider&lt;/code&gt; will automatically map the tenancy events to the listener, which will create the tenant's database and run the tenant-specific migrations inside the &lt;code&gt;database/migrations/tenant&lt;/code&gt; directory for the new tenant.&lt;/p&gt;

&lt;p&gt;In a nutshell, the controller has three methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;showRegistrationForm()&lt;/code&gt;: Displays the tenant registration form&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;register()&lt;/code&gt;: Registers a new tenant, which creates a new tenant record and domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;registered()&lt;/code&gt;: Displays a success message after registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This controller will be used to manage tenant registration in our application. Allowing new tenants to register and create their own subdomain and database for their account.&lt;/p&gt;

&lt;p&gt;Add routes for tenant registration in &lt;code&gt;routes/web.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\TenantController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;TenantController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'showRegistrationForm'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.register'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;TenantController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'register'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/registered/{domain}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;TenantController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'registered'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.registered'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the corresponding views for tenant registration starting by creating the &lt;code&gt;resources/views/tenant/register.blade.php&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-guest-layout&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{{ route('tenant.register') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @csrf
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-input-label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"domain"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"__('Subdomain')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;x-text-input&lt;/span&gt;
          &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"domain"&lt;/span&gt;
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-1 block w-full"&lt;/span&gt;
          &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
          &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"domain"&lt;/span&gt;
          &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"old('domain')"&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600 ml-2 mt-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;.example.com&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 flex items-center justify-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-primary-button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ml-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; {{ __('Register Tenant') }} &lt;span class="nt"&gt;&amp;lt;/x-primary-button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/x-guest-layout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the &lt;code&gt;resources/views/tenant/registered.blade.php&lt;/code&gt; file to display the success message after registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-guest-layout&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600 mb-4 text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {{ __('Your tenant has been registered successfully!') }}
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 flex items-center justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      Your tenant URL:
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt;
        &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://{{ $domain }}.example.com"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600 hover:text-gray-900 text-sm underline"&lt;/span&gt;
        &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;https://{{ $domain }}.example.com&lt;span class="nt"&gt;&amp;lt;/a&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/x-guest-layout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This completes the tenant registration process. Tenants can now register and create their own subdomain and database for their account. In a real-world scenario, you would protect the registration routes with authentication middleware to ensure that only authorized admin users can create new tenants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying Tenant Registration
&lt;/h3&gt;

&lt;p&gt;To verify that the registration process works, visit &lt;code&gt;http://laravel-multi-tenant-saas.test/register&lt;/code&gt; and register a new tenant. After registration, you should see the success message with the tenant's domain.&lt;/p&gt;

&lt;p&gt;Next go to your Neon dashboard and verify that the new tenant's database has been created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the newly created tenant in the &lt;code&gt;tenants&lt;/code&gt; table. You can also check the &lt;code&gt;domains&lt;/code&gt; table to verify that the tenant's domain has been added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to verify that you actually have a separate database for the new tenant, use the &lt;code&gt;\l&lt;/code&gt; command in the &lt;code&gt;psql&lt;/code&gt; console to list all databases or the following SQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_database&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;datistemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tenant's database should be listed in the results and it should be named &lt;code&gt;tenant{tenant_id}&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The tenancy package allows you to configure the database naming convention for tenants. By default, the database name is &lt;code&gt;tenant{tenant_id}&lt;/code&gt; where &lt;code&gt;{tenant_id}&lt;/code&gt; is the ID of the tenant. You can also configure the package to use separate schemas instead of separate databases for tenants.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With that done, you've successfully implemented tenant registration in your multi-tenant SaaS application. Next let's implement the tenant onboarding process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Tenant Onboarding
&lt;/h3&gt;

&lt;p&gt;Now that you can register new tenants, let's create an onboarding process.&lt;/p&gt;

&lt;p&gt;Each tenant will need to create an account to access their dashboard. The domain will be used to identify the tenant, so we'll use the domain as the tenant's subdomain, e.g., &lt;code&gt;tenant1.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a new controller for tenant onboarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:controller Tenant/OnboardingController
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;app/Http/Controllers/Tenant/OnboardingController.php&lt;/code&gt; to handle the onboarding process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers\Tenant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnboardingController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.dashboard'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.onboarding'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|max:255'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|email|max:255|unique:users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|min:8|confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.dashboard'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Welcome to your new account!'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add routes for the onboarding process in &lt;code&gt;routes/tenant.php&lt;/code&gt; inside the &lt;code&gt;Route::middleware&lt;/code&gt; group for tenant routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Tenant\OnboardingController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'web'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;InitializeTenancyByDomain&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;PreventAccessFromCentralDomains&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Existing routes&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/onboarding'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;OnboardingController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'show'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.onboarding'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/onboarding'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;OnboardingController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'store'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.onboarding.store'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Create the onboarding view in &lt;code&gt;resources/views/tenant/onboarding.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-guest-layout&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"{{ route('tenant.onboarding.store') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @csrf

    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-input-label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"__('Name')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-text-input&lt;/span&gt;
        &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-1 block w-full"&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
        &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
        &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"old('name')"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="na"&gt;autofocus&lt;/span&gt;
        &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
      &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-input-label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"__('Email')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-text-input&lt;/span&gt;
        &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-1 block w-full"&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"old('email')"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
      &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-input-label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"__('Password')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-text-input&lt;/span&gt;
        &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-1 block w-full"&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
        &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"new-password"&lt;/span&gt;
      &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-input-label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password_confirmation"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"__('Confirm Password')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-text-input&lt;/span&gt;
        &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password_confirmation"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-1 block w-full"&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
        &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password_confirmation"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"new-password"&lt;/span&gt;
      &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 flex items-center justify-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;x-primary-button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ml-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; {{ __('Complete Setup') }} &lt;span class="nt"&gt;&amp;lt;/x-primary-button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/x-guest-layout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simplicity, we're extending the Breeze guest layout for the onboarding form. But you can customize the layout to match your application's design and even have different layouts for the onboarding process based on each tenant's requirements.&lt;/p&gt;

&lt;p&gt;To test the onboarding process, visit &lt;code&gt;http://tenant1.example.com/onboarding&lt;/code&gt; and complete the onboarding form. After submitting the form, you should be redirected to the tenant dashboard which we'll implement next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Tenant Dashboard
&lt;/h3&gt;

&lt;p&gt;Create a new controller for the tenant dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:controller Tenant/DashboardController
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;app/Http/Controllers/Tenant/DashboardController.php&lt;/code&gt; to display the tenant dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers\Tenant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DashboardController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.dashboard'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the dashboard view in &lt;code&gt;resources/views/tenant/dashboard.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;x-app-layout&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;x-slot&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-800 text-xl font-semibold leading-tight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ __('Dashboard') }}&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/x-slot&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"py-12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto max-w-7xl lg:px-8 sm:px-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"overflow-hidden bg-white shadow-sm sm:rounded-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-900 p-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ __("You're logged in!") }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/x-app-layout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a route for the tenant dashboard in &lt;code&gt;routes/tenant.php&lt;/code&gt; inside the &lt;code&gt;Route::middleware&lt;/code&gt; group for tenant routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Tenant\DashboardController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'web'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;InitializeTenancyByDomain&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;PreventAccessFromCentralDomains&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Existing routes&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DashboardController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant.dashboard'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;To test the tenant dashboard, visit &lt;code&gt;http://tenant1.example.com/dashboard&lt;/code&gt; after completing the onboarding process. You should see the dashboard view with a welcome message.&lt;/p&gt;

&lt;p&gt;You can also check the &lt;code&gt;users&lt;/code&gt; table in the tenant's database to verify that the user account created during onboarding has been added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will show you the user account created during the onboarding process for that specific tenant in the tenant's database rather than the central database.&lt;/p&gt;

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

&lt;p&gt;In this tutorial, we've built a simple multi-tenant application using Laravel and Neon. We've covered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setting up the project and implementing multi-tenancy&lt;/li&gt;
&lt;li&gt;Creating a tenant registration process&lt;/li&gt;
&lt;li&gt;Implementing tenant onboarding&lt;/li&gt;
&lt;li&gt;Adding a tenant dashboard for individual tenants&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This implementation provides a foundation for building more complex SaaS applications with Laravel and Neon. You can further expand on this system by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding more features to the tenant dashboard&lt;/li&gt;
&lt;li&gt;Implementing billing and subscription management&lt;/li&gt;
&lt;li&gt;Enhancing security with two-factor authentication&lt;/li&gt;
&lt;li&gt;Adding more tenant-specific customizations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using the &lt;code&gt;stancl/tenancy&lt;/code&gt; package along with Neon, each tenant will have its own database. Thanks to Neon's autoscaling feature, you can easily scale your application as you onboard more tenants.&lt;/p&gt;

&lt;p&gt;There are other packages and tools available to help you build multi-tenant applications with Laravel. You can explore these options based on your requirements and choose the one that best fits your needs. Some of the popular packages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spatie.be/docs/laravel-multitenancy/v3/introduction" rel="noopener noreferrer"&gt;spatie/laravel-multitenancy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tenancy/tenancy" rel="noopener noreferrer"&gt;tenancy/tenancy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs" rel="noopener noreferrer"&gt;Laravel Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tenancyforlaravel.com/" rel="noopener noreferrer"&gt;stancl/tenancy Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.tech/docs/" rel="noopener noreferrer"&gt;Neon Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How to use Synthetic Data to catch more bugs with Neosync</title>
      <dc:creator>Rishi Raj Jain</dc:creator>
      <pubDate>Thu, 19 Dec 2024 15:06:37 +0000</pubDate>
      <link>https://dev.to/neon-postgres/how-to-use-synthetic-data-to-catch-more-bugs-with-neosync-1n2m</link>
      <guid>https://dev.to/neon-postgres/how-to-use-synthetic-data-to-catch-more-bugs-with-neosync-1n2m</guid>
      <description>&lt;p&gt;A guide to developing robust, scalable and secure applications with Synthetic Data&lt;/p&gt;

&lt;p&gt;In today’s world, developers are under more pressure than ever to develop robust, scalable and secure applications that have no bugs and are performant. Users want their applications and software to work flawlessly and any sign of a bug or disruption in workflow can lead to a frustrated or even lost user. &lt;/p&gt;

&lt;p&gt;The challenge is that it’s really hard and time consuming to manually write test cases and test data in order to try to catch every edge case. Even then, it’s not perfect! A lot of teams will use their production data to test locally but this has a lot of privacy and security risks and by definition, you’re only using data that you’ve been able to successfully process. Which means that you’re not catching additional edge cases.&lt;/p&gt;

&lt;p&gt;So then, as a developer, how can you be sure that you’re writing resilient code and developing a resilient application if you only have “happy path” data? The answer is that you really can’t. You need a better way to test and validate your systems. &lt;/p&gt;

&lt;p&gt;This is where synthetic data can help you catch more bugs before you ship to production. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is Synthetic Data?
&lt;/h2&gt;

&lt;p&gt;Synthetic data is artificially generated data that is statistically and structurally just like real-world data, without containing any actual sensitive or personal information. Synthetic data is typically created using a combination of machine learning models and simulation algorithms. &lt;/p&gt;

&lt;p&gt;This data can be nearly statistically identical to the source data set. For example, if the source data set has an age column with a range of 70 and a mean of 25.8, we can use machine learning models to create an identical column of brand new data that also has a range of 70 and mean of 25.8. Machine learning models such as &lt;a href="https://proceedings.neurips.cc/paper_files/paper/2019/file/254ed7d2de3b23ab10936522dd547b78-Paper.pdf" rel="noopener noreferrer"&gt;CTGAN &lt;/a&gt;are great at learning the statistical properties of a set of data and replicating them with brand new data. &lt;/p&gt;

&lt;p&gt;Further, with synthetic data, we can replicate the structure of the source data set. This means preserving the &lt;a href="https://www.neosync.dev/blog/referential-integrity" rel="noopener noreferrer"&gt;referential integrity&lt;/a&gt; or primary key and foreign key relationships, circular dependencies and other constraints. &lt;/p&gt;

&lt;p&gt;Using a combination of machine learning and other algorithms we can replicate this structure in a data set such that you could easily import it into your current database without any problems. This is the power of synthetic data. The ability to create a data set that is statistically and structurally just like your production data without any of the privacy and security concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Synthetic data to catch more bugs
&lt;/h2&gt;

&lt;p&gt;Synthetic data can be a powerful tool to catch more bugs before they get reported in production. Let’s take a look at three of the most common use cases. &lt;/p&gt;

&lt;h3&gt;
  
  
  Generating diverse data to catch edge cases
&lt;/h3&gt;

&lt;p&gt;Test driven development is more popular than ever which is a great thing for application resiliency. But the truth is that manually writing test cases can be tedious and really hard to anticipate every edge case. Most developers will test the happy path and then some edge cases before moving on.&lt;/p&gt;

&lt;p&gt;This is where synthetic data can help. By generating a large and diverse set of data that would have taken days to manually write, you can check to make sure that your application can handle many different data types, formats and values. &lt;/p&gt;

&lt;p&gt;The best part is that generating data can be really fast. For example, using Neosync, you can generate 10,000 rows of data for your Neon database in less than 30 seconds. It’s as simple as creating a connection to your Neon database and creating a job in Neosync. For more information, check out this blog post. &lt;/p&gt;

&lt;p&gt;Ultimately, the end goal is that you can catch more bugs in your development and staging environments. Which means that you can ship a more resilient application. &lt;/p&gt;

&lt;h3&gt;
  
  
  Using Synthetic data for automated testing
&lt;/h3&gt;

&lt;p&gt;Synthetic data can take automated testing to the next level. Developers can set up automated test suites in their CI pipelines and point to a database that is hydrated with synthetic data to test their applications. Instead of using the same stale data set, developers can have brand new data generated every time, increasing the chances that they catch a bug before they ship to production. &lt;/p&gt;

&lt;p&gt;If they do catch a bug, then they can replicate that bug locally by hydrating a development or local database with the same data set and start to debug the error. This workflow of testing locally with synthetic data and then testing in CI with synthetic data, is a great way to catch bugs and gain confidence in your application’s resiliency. &lt;/p&gt;

&lt;h3&gt;
  
  
  Using Synthetic data to for performance testing
&lt;/h3&gt;

&lt;p&gt;The last thing any developer wants is for their application to crash during a period of high-traffic. Imagine if Shopify crashed during Black Friday? For a company like Shopify, the problem is that they have a lot of sensitive data and can’t move their production data around easily without taking on a lot of privacy and security risk. So how do they get enough data for performance testing without worrying about privacy or security?&lt;/p&gt;

&lt;p&gt;This is a perfect use case for synthetic data. Because synthetic data is artificially generated, you can create as much of it as you’d like. This makes performance testing a breeze and eliminates the privacy and security risks of handling production data.  Just define how much data you need and then let the system create that data for you. You can then automatically ingest that into your application and monitor any performance issues without having to wait to see if it fails in production.&lt;/p&gt;

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

&lt;p&gt;Synthetic data is a great tool that developers can use to catch more bugs and ship more resilient applications. It’s a powerful way to test your applications functionality and performance without any of the privacy and security concerns of production data. The great thing is that synthetic data tools and platforms are only getting better, higher fidelity and faster which makes it even more compelling for developers to start considering it as part of their core workflow. &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Wrapping My Mind Around Node.js Runtimes</title>
      <dc:creator>David Gomes</dc:creator>
      <pubDate>Fri, 22 Nov 2024 22:34:32 +0000</pubDate>
      <link>https://dev.to/neon-postgres/wrapping-my-mind-around-nodejs-runtimes-4elp</link>
      <guid>https://dev.to/neon-postgres/wrapping-my-mind-around-nodejs-runtimes-4elp</guid>
      <description>&lt;h3&gt;
  
  
  A look at Deno and Bun and how they work differently from Node.js
&lt;/h3&gt;

&lt;p&gt;There’s been a lot of innovation in the last few years in Node.js runtimes. Node.js used to be the only viable option, but now there’s other contenders such as Bun and Deno. But are they ready for prime time? When should they be used, and by whom?&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait, what even is a JavaScript runtime?
&lt;/h2&gt;

&lt;p&gt;Good question. JavaScript wasn’t really built for the server. In fact, JavaScript wasn’t really “built” at all. What we think of as JS is just a specification.&lt;/p&gt;

&lt;p&gt;A runtime is what brings that specification to life. It’s the environment that executes JavaScript code. When you’re working in a browser, the browser provides the runtime. But on a server, you need something else. That’s where Node.js came in and where these new runtimes are shaking things up.&lt;/p&gt;

&lt;p&gt;A JavaScript runtime typically consists of several key components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;strong&gt;JavaScript Engine.&lt;/strong&gt; This is the core that interprets and executes JavaScript code. V8 (used by Node.js and Chrome) is a famous example.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Event loop.&lt;/strong&gt; Manages asynchronous operations, allowing non-blocking I/O operations.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;APIs.&lt;/strong&gt; Provides interfaces for system-level operations like file I/O, networking, and timers.&lt;/li&gt;
&lt;li&gt;    Standard library. A set of built-in modules and functions that developers can use out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node’s ability to handle thousands of concurrent connections with its non-blocking I/O model has made it particularly popular for real-time applications, APIs, and microservices architectures. And it is still the most popular “web technology” according to the &lt;a href="https://survey.stackoverflow.co/2024" rel="noopener noreferrer"&gt;2024 Stack Overflow survey&lt;/a&gt;:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The problems with Node.js
&lt;/h2&gt;

&lt;p&gt;But Node is not without its detractors—including the person who initially built Node, &lt;a href="https://x.com/rough__sea" rel="noopener noreferrer"&gt;Ryan Dahl&lt;/a&gt;. In a 2019 jsconf talk titled &lt;a href="https://www.youtube.com/watch?v=M3BM9TB-8yA" rel="noopener noreferrer"&gt;10 Things I Regret About Node.js&lt;/a&gt;, Dahl lists the problems he now sees in Node, such as…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;strong&gt;Security.&lt;/strong&gt; By default, the module system in Node.js allows any package to access the file system, network, and environment variables. This poses significant security risks, especially when using third-party modules.&lt;/li&gt;
&lt;li&gt;   &lt;strong&gt;Package management.&lt;/strong&gt; The package.json file, npm, and the Node modules system have become overly complex. There’s also various security issues around npm (&lt;a href="https://stacklok.com/blog/how-npm-install-scripts-can-be-weaponized-a-real-life-example-of-a-harmful-npm-package" rel="noopener noreferrer"&gt;like the postinstall scripts&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Build system complexity.&lt;/strong&gt; Node’s build system, particularly for native modules, has become unnecessarily complicated over time. Basically, supporting both CommonJS and ES modules made Node.js much more complex.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, all this together has led a few people to think they can do better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new runtimes in town
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deno: Security-oriented, modern JavaScript
&lt;/h3&gt;

&lt;p&gt;Ryan Dahl chose to take the learnings from Node and built &lt;a href="https://deno.com/" rel="noopener noreferrer"&gt;Deno&lt;/a&gt;. Deno addresses many of the concerns he had with Node.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;strong&gt;Focus on security.&lt;/strong&gt; Deno runs code in a sandbox by default. Accessing the file system, network, or environment variables requires explicit permissions. This approach significantly reduces the risk of malicious code execution.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Built-in TypeScript support.&lt;/strong&gt; Unlike Node.js, which requires additional setup for TypeScript, Deno supports TypeScript out of the box. This integration streamlines development and improves code quality.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Standard modules.&lt;/strong&gt; Deno provides a standard library of high-quality, audited modules. This reduces dependency on third-party packages for common functionalities.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Browser compatibility.&lt;/strong&gt; Deno aims to be as close as possible to browser JavaScript. It uses web standard APIs where possible, making it easier for developers to write code that works both in the browser and on the server.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Import URLs.&lt;/strong&gt; Instead of using a package manager like npm, Deno allows modules to be imported directly via URLs.&lt;/li&gt;
&lt;li&gt;        This will somewhat change with Deno v2, more on this later.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Single executable.&lt;/strong&gt; Deno ships as a single executable with no external dependencies, making it easy to install and use across different environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deno is gaining traction, especially among developers who appreciate its modern features and security-first approach. It’s particularly attractive for projects that can benefit from its TypeScript integration and don’t rely heavily on Node-specific libraries. &lt;/p&gt;

&lt;p&gt;Deno also provides infrastructure beyond the runtime. Tests, linters, and formatters are also included. You can also deploy your applications using Deno’s serverless environment, Deno Deploy (learn &lt;a href="https://neon.tech/docs/guides/deno" rel="noopener noreferrer"&gt;how to use Neon with Deno Deploy&lt;/a&gt;), and build entire applications using Deno’s framework, &lt;a href="https://fresh.deno.dev/" rel="noopener noreferrer"&gt;Fresh&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a simple setup with Deno to use Neon. The Deno-specific things are the import &lt;code&gt;(@neon/serverless is called @neondatabase/serverless in NPM)&lt;/code&gt; and the &lt;code&gt;Deno.env.get&lt;/code&gt; bits. Also, this is a Deno v2 example (which is currently in “canary”).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { neon } from "@neon/serverless";

// Get the connection string from the environment variable "DATABASE_URL"
const databaseUrl = Deno.env.get("DATABASE_URL")!;

if (!databaseUrl) {
  throw new Error(`Please add a DATABASE_URL environment variable.`);
}

const sql = neon(databaseUrl);

const rows = await sql`SELECT version()`;

console.log(rows[0].version);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run this, call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=$DATABASE_URL deno test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(And notice how Deno will ask for permission to read environment variables and make network requests &lt;a href="https://docs.deno.com/runtime/fundamentals/security/" rel="noopener noreferrer"&gt;at runtime.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;To show the speed of progress with Deno, up until a few months ago, the above code would have used an import URL, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import * as postgres from 'https://deno.land/x/postgres@v0.17.0/mod.ts';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deno just released version &lt;a href="https://deno.com/blog/v1.46" rel="noopener noreferrer"&gt;1.46,&lt;/a&gt; which will be the last 1.X version. Deno 2 is changing in response to feedback from the community and a greater understanding of the initial strict choices of the runtime.&lt;/p&gt;

&lt;p&gt;Take imports. Originally the idea was to have simple URLs like &lt;a href="https://deno.land/x/postgres@v0.17.0/mod.ts" rel="noopener noreferrer"&gt;https://deno.land/x/postgres@v0.17.0/mod.ts&lt;/a&gt;. As Deno put it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Designing Deno’s module system around HTTP imports was ambitious. It aimed to replace npm with a distributed system over HTTP, aligning with how ES Modules work in browsers. This eliminated the need for package.json files and node_modules folders, simplifying project structures.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the reality wasn’t as clean. Deno found that dependency management, clutter, and reliability were all a problem with this approach. So, they’ve launched &lt;a href="https://jsr.io/" rel="noopener noreferrer"&gt;JSR&lt;/a&gt;, the JavaScript Registry. This is akin to npm in node, but TypeScript and ECMAScript-native. All you have to do is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deno add @neon/serverless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you’re ready to roll.&lt;/p&gt;

&lt;p&gt;Like Node.js, Deno is built on the V8 engine. Originally, Deno wasn’t supposed to be compatible with Node.js. But, as with import URLs, pragmatism beat out idealism. The Deno team realized that compatibility with the vast Node.js ecosystem would significantly increase Deno’s adoption and usefulness. As a result, they’ve been working on Node.js compatibility layers, allowing developers to use many Node.js packages and APIs within Deno projects, thereby bridging the gap between the two runtimes.&lt;/p&gt;

&lt;p&gt;If you want to learn more, I highly recommend &lt;a href="https://www.youtube.com/watch?v=tZBCq8Ijkgw" rel="noopener noreferrer"&gt;the recent participation of Ryan Dahl on the “Syntax” podcast&lt;/a&gt; to learn more about Deno v2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bun: A performance-focused Node replacement
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bun.sh/" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; is a little different. It was Node-compatible out of the gate and not looking to right-the-wrongs of Node.js. But, as it grew, it started to evolve towards a slightly different direction.&lt;/p&gt;

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

&lt;p&gt;Benchmarking is hard. We at Neon are definitely very well aware of that, but my anecdotal experience is that Bun is indeed really fast. How fast exactly is very hard to measure, of course.&lt;/p&gt;

&lt;p&gt;Like Deno, Bun is looking to rely on JavaScript primitives and code simplicity. Here’s &lt;a href="https://github.com/neondatabase/examples/tree/main/with-bun" rel="noopener noreferrer"&gt;Neon running with Bun&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { neon } from "@neondatabase/serverless";

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
  throw new Error(`Please add a DATABASE_URL environment variable.`);
}

const sql = neon(databaseUrl);

const rows = await sql`SELECT version()`;

console.log(rows[0].version);

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

&lt;/div&gt;



&lt;p&gt;To call this, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=$DATABASE_URL bun test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bun’s approach to improving the JavaScript runtime environment focuses on several key areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;strong&gt;Performance.&lt;/strong&gt; Built on the JavaScriptCore engine (used by Safari), Bun is designed for speed from the ground up. Its fast startup times and efficient execution make it particularly suitable for serverless environments and microservices.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;TypeScript support.&lt;/strong&gt; Similar to Deno, Bun offers out-of-the-box TypeScript support. However, Bun takes it a step further by transpiling TypeScript at runtime, eliminating the need for separate build steps in many cases.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Enhanced APIs.&lt;/strong&gt; Bun provides additional APIs that aren’t available in Node.js, such as the Bun.file API for efficient file operations and the SQLite API for built-in database functionality.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;Streamlined configuration.&lt;/strong&gt; Bun aims to reduce configuration overhead. For instance, it automatically loads environment variables from .env files, simplifying local development and deployment processes.&lt;/li&gt;
&lt;li&gt;    &lt;strong&gt;All-in-one toolkit.&lt;/strong&gt; Bun isn’t just a runtime; it’s a complete toolkit. It includes a package manager, test runner (which &lt;a href="https://nodejs.org/api/test.html" rel="noopener noreferrer"&gt;Node.js now has as well&lt;/a&gt;), and bundler, all optimized for performance. This integrated approach simplifies the development workflow and reduces the need for additional tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Bun team is rapidly iterating and adding some interesting features. For example, they introduced “&lt;a href="https://bun.sh/guides/http/hot" rel="noopener noreferrer"&gt;hot module reloading&lt;/a&gt;” for server-side code, a feature that’s been long desired in the Node.js ecosystem. Also, more recently, they added support for &lt;a href="https://x.com/jarredsumner/status/1825113318539006402" rel="noopener noreferrer"&gt;C code using TinyCC&lt;/a&gt; 🤯And I believe they led the way with &lt;a href="https://bun.sh/docs/api/sqlite" rel="noopener noreferrer"&gt;SQLite embedding&lt;/a&gt; (but Node.js has now caught up).&lt;/p&gt;

&lt;p&gt;While Bun is still young compared to Node.js and even Deno, its focus on performance and developer experience is attracting attention. It is particularly appealing for projects where raw speed is a priority, such as high-traffic web servers or compute-intensive apps.&lt;/p&gt;

&lt;p&gt;Furthermore, it’s also extremely appealing to use Bun not as your production runtime, but just as your package manager. We’re currently doing that with &lt;a href="https://github.com/neondatabase/neonctl" rel="noopener noreferrer"&gt;neonctl&lt;/a&gt;, purely because of Bun’s performance (we also want to migrate the CLI runtime to Bun at some point, due to its single-file &lt;a href="https://bun.sh/docs/bundler/executables" rel="noopener noreferrer"&gt;executable feature&lt;/a&gt; and overall performance).&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;First of all, I have to mention that there are a few other runtimes that are probably worth mentioning. Although, perhaps it’d be better to refer to some of them as “neo-runtimes” since they’re not necessarily full blown runtimes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    &lt;a href="https://github.com/saghul/txiki.js/" rel="noopener noreferrer"&gt;https://github.com/saghul/txiki.js/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;a href="https://github.com/cloudflare/workerd" rel="noopener noreferrer"&gt;https://github.com/cloudflare/workerd&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, there’s the aspect of picking a runtime. This is a very hard decision and obviously migrating existing projects is even harder. But this is my very personal current thought process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;    Use Node.js for anything that needs to live for really long, or doesn’t need Deno’s &lt;a href="https://docs.deno.com/runtime/fundamentals/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;    Use Deno if you want extra security, or if you specifically want to use &lt;a href="https://deno.com/deploy" rel="noopener noreferrer"&gt;Deno Deploy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;        Don’t use Deno if all you care about is not transpiling TypeScript. Node.js will support TypeScript soon enough.&lt;/li&gt;
&lt;li&gt;    Use Bun if performance is the most important thing to you. Or, if you want to use some of Bun’s more “fun” features (Macros, embedded SQLite, etc.) and you don’t have a super critical requirement that your project outlives your time on Earth (Bun is afterall backed by a very early stage company).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s basically it, but I know this is all very subjective — do your own research! I really love Bun’s performance and I try to use Bun for all package management on my local machine. However, I would only use Bun as a production runtime under certain circumstances for now.&lt;/p&gt;

&lt;p&gt;If you’re looking for a more pessimistic take on new runtimes, I can recommend this &lt;a href="https://dev.to/thejaredwilcurt/bun-hype-how-we-learned-nothing-from-yarn-2n3j"&gt;blog post&lt;/a&gt;. But I tend to have a more positive outlook on new companies building cool new dev tooling.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>node</category>
    </item>
    <item>
      <title>Looking at How Replit Agent Handles Databases</title>
      <dc:creator>Sam シ</dc:creator>
      <pubDate>Wed, 13 Nov 2024 16:16:59 +0000</pubDate>
      <link>https://dev.to/neon-postgres/looking-at-how-replit-agent-handles-databases-4259</link>
      <guid>https://dev.to/neon-postgres/looking-at-how-replit-agent-handles-databases-4259</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.replit.com/replitai/agent" rel="noopener noreferrer"&gt;Replit Agent&lt;/a&gt; is a powerful tool within the &lt;a href="https://replit.com/" rel="noopener noreferrer"&gt;Replit&lt;/a&gt; development environment that allows you to offload coding tasks using natural language and AI. By interacting with the Replit Agent, you can generate code, create applications, and modify existing features simply by asking for what you need. &lt;/p&gt;

&lt;p&gt;This automation becomes particularly valuable when you’re working on full-stack applications that require database changes, where it helps maintain consistency between the code and the database schema. Essentially, you can have the Replit Agent implement end-to-end features within your application, changing the API, Database, and Frontend all at once. Getting Replit Agents to handle database migrations&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing the tools
&lt;/h2&gt;

&lt;p&gt;To manage your app’s data, Replit provisions a database, a fully managed serverless Postgres database (powered by Neon) that scales on demand. The Agent builds the rest of the app in &lt;code&gt;Flask&lt;/code&gt;, a python microframework for web applications which supports API endpoints and HTML templating.&lt;/p&gt;

&lt;p&gt;With a little prompting, you can get the Agent to use Alembic, a database migration tool that works with SQLAlchemy. SQLAlchemy acts as an Object Relational Mapper (ORM), which helps convert your Python objects and code into the schema used by your Postgres database. Alembic tracks changes to your SQLAlchemy models and automatically generates migration scripts for database schema updates.&lt;/p&gt;

&lt;p&gt;In this article, we will explore how this process works by creating a simple TODO application where features are added incrementally using only natural language prompts to the Replit Agent. You’ll see how you can get Alembic migrations to be automatically generated and how changes are reflected in your Neon database. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To follow along, you need access to the Replit Agent, which is available on the Replit Core or Team subscriptions.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the project
&lt;/h2&gt;

&lt;p&gt;To begin, head over to your Replit home dashboard and create a new &lt;code&gt;Repl&lt;/code&gt;. Instead of setting everything up manually, simply describe the project you want in the Replit Agent textbox. To follow this article, ask the agent to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;create me a TODO application API with CRUD functionalities and a simple frontend to view the tasks&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;Once you describe the project, Replit Agent will generate a plan that includes the core features and may also suggest additional features or enhancements that could be useful. You can review these suggestions and make adjustments if needed. After you approve the plan, the agent will provision a Neon PostgreSQL database, generate the SQLAlchemy schema for your application, set up the Flask API endpoints, and create the HTML and JavaScript for the frontend.&lt;/p&gt;

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

&lt;p&gt;Within a minute or two, the Replit Agent will finish setting up the fully functional application. At this point, it will prompt you to test everything to make sure it’s working as expected. You should try adding a simple task to the app and confirm that the basic functionality is correct. Once you verify that everything is in order, the Agent will create a commit and a checkpoint for the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a due date feature
&lt;/h2&gt;

&lt;p&gt;With the basic TODO app up and running, you can extend its functionality by allowing users to optionally specify a due date for each task to help with setting deadlines or reminders. To add this feature, simply ask the Replit Agent:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;can you add another text box so that the user can optionally add a due date to the task&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The Replit Agent understands that this feature requires changes to both the frontend (adding a date input) and the backend (modifying the database schema to store the due date). Specifically, a new column needs to be added to the tasks table to store the due_date.&lt;/p&gt;

&lt;p&gt;Here’s how the Agent handles the database update process:&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating the SQLAlchemy model update
&lt;/h3&gt;

&lt;p&gt;The agent modifies the Task model to include a new due_date field. The updated model now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    class Task(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        title = db.Column(db.String(100), nullable=False)
        description = db.Column(db.Text, nullable=True)
        completed = db.Column(db.Boolean, default=False)
        due_date = db.Column(db.DateTime, nullable=True)


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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating and applying alembic migrations
&lt;/h3&gt;

&lt;p&gt;Since this change alters the database schema, the Replit Agent generates a migration script using Alembic. The migration will add the due_date column to the tasks table in the database. Here’s the up migration script that adds the due_date column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  """Add due_date to Task model

    Revision ID: 0f4af0e04d02
    Revises:
    Create Date: 2024-10-15 17:40:22.891574

    """
    from alembic import op
    import sqlalchemy as sa


    # revision identifiers, used by Alembic.
    revision = '0f4af0e04d02'
    down_revision = None
    branch_labels = None
    depends_on = None


    def upgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        with op.batch_alter_table('task', schema=None) as batch_op:
            batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))

        # ### end Alembic commands ###


    def downgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        with op.batch_alter_table('task', schema=None) as batch_op:
            batch_op.drop_column('due_date')

        # ### end Alembic commands ###

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

&lt;/div&gt;



&lt;p&gt;Notice that it provides both an upgrade and downgrade migration such that it can also handle rollbacks to previous states. &lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend update
&lt;/h3&gt;

&lt;p&gt;The Agent also updates the frontend to include a new text box where users can input a due date when creating or updating a task, as seen here:&lt;/p&gt;

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

&lt;p&gt;After you confirm the update, the &lt;code&gt;due_date&lt;/code&gt; field is now part of both the UI and the database, and the agent ensures the changes are committed properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding more features: Priority field
&lt;/h2&gt;

&lt;p&gt;You can continue adding features to your TODO app in a similar way. For example, to help users manage the importance of their tasks, you might want to add a priority field to each task. Like before, asking Replit Agent to implement this will trigger another schema change, and it will generate and apply the relevant Alembic migrations automatically:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;add a priority flag to the task creation that can be low, medium, or high and that shows up on the tasks list&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Once the agent has processed the request, it will update both the backend and frontend. The priority field will now appear in the task creation form, and tasks will display their priority (low, medium, or high) in the tasks list:&lt;/p&gt;

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

&lt;p&gt;At this point, you have introduced two schema changes to the database: one for the &lt;code&gt;due_date&lt;/code&gt; field and one for the &lt;code&gt;priority&lt;/code&gt; field. With each change, Alembic generated a separate migration file. These migration files represent individual checkpoints in the evolution of your database schema.&lt;/p&gt;

&lt;p&gt;When you look into the &lt;code&gt;migrations/versions&lt;/code&gt; folder of your project, you’ll now see two distinct migration files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;     &lt;code&gt;{commit-id}_add_due_date_to_task.py – This migration was created when the due_date field was added.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;    &lt;code&gt;{commit-id}_add_priority_to_task.py – This migration was created when the&lt;/code&gt;priority&lt;code&gt;field was added.&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;These migration files are important because they allow you to easily rollback your database schema to any previous state. For example, if you want to undo the addition of the priority field but keep the &lt;code&gt;due_date&lt;/code&gt;, you can apply a rollback to the previous migration using Alembic’s rollback functionality.&lt;/p&gt;

&lt;p&gt;By having separate migrations for each change, you can always track the evolution of your schema and revert specific changes when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling back
&lt;/h2&gt;

&lt;p&gt;There may come a time when you’re not happy with how a feature turned out, or you simply don’t need it anymore. This is where Replit’s rollback feature, powered by Alembic, becomes especially useful. Not only can you revert the application to a previous state, but you can also undo the database changes, ensuring everything stays consistent.&lt;/p&gt;

&lt;p&gt;In this case, let’s say you no longer need the priority field or you want to remove it for some reason. Rolling back the application and database to the state before the priority field was added is straightforward.&lt;/p&gt;

&lt;p&gt;Replit Agent will automatically run the downgrade migration script, and the priority field will be removed from the database. Additionally, the app’s frontend will be updated, and the feature will no longer appear in the task creation form or task list.&lt;/p&gt;

&lt;p&gt;Here’s the UI after the rollback, which now looks just like it did before the priority field was added:&lt;/p&gt;

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

&lt;p&gt;To confirm that the database has been successfully rolled back, you can use the Postgres tool in the Replit development environment to inspect the tasks table. As shown below, the priority column is no longer present:&lt;/p&gt;

&lt;p&gt;This means the database schema has been fully reverted to its earlier state, and the feature has been removed not just from the code, but also from the database. Because Alembic tracks every migration in the project, you can roll back your schema to any previous migration—whether that’s the most recent change or a much earlier one. The migration files in the migrations folder act like checkpoints in your database’s timeline. Each migration has a unique revision ID, which allows you to target a specific state when rolling back.&lt;/p&gt;

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

&lt;p&gt;Replit Agents can use Alembic and Neon PostgreSQL to handle database changes efficiently. When you request a new feature that affects the database, you can get the agent to generate a migration files with two functions—one for applying changes and another for rolling them back. This makes it easy to update or revert your database schema as needed.&lt;/p&gt;

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

&lt;p&gt;By creating these files with each change, Replit ensures that your database stays in sync with your application. Whether you’re adding new fields, removing features, or undoing changes, the agent manages the migrations so you don’t have to worry about keeping the database consistent. This allows you to focus on building your application while the underlying structure is handled for you.&lt;/p&gt;

&lt;p&gt;Neon is the serverless Postgres database backing Replit Agents. &lt;a href="https://console.neon.tech/realms/prod-realm/protocol/openid-connect/registrations?client_id=neon-console&amp;amp;redirect_uri=https%3A%2F%2Fconsole.neon.tech%2Fauth%2Fkeycloak%2Fcallback&amp;amp;response_type=code&amp;amp;scope=openid+profile+email&amp;amp;state=O0vePLuUjTzGPk1PdOEDXA%3D%3D%2C%2C%2C" rel="noopener noreferrer"&gt;Start using it for free&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>programming</category>
      <category>ai</category>
      <category>database</category>
    </item>
  </channel>
</rss>
