Role-Based AI Inventory Chatbot
Build a JWT-secured Spring Boot chatbot that queries MySQL and answers with Gemini AI.
Introduction
30 Second Summary
Every business sits on data that could answer questions instantly. The challenge is turning raw database rows into conversational answers that anyone on the team can understand.
In this project, you will build an AI-powered inventory chatbot using Spring Boot and Google Gemini. Your backend queries a MySQL database and feeds the results to Gemini so it can answer inventory questions in natural language.
What You'll Build
Ask "What are the total sales?" in the browser chat, and watch Gemini's answer change completely depending on whether you logged in as an admin or a customer.
By the end of this project, you'll have:
- A browser chat interface where you type inventory questions and receive AI-generated conversational answers drawn from your MySQL database.
- JWT-secured login that distinguishes admins from customers. Ask the same question as each role and watch Gemini give completely different answers.
- A data bridge pattern where your backend queries the database, injects the results into the Gemini prompt, and off-topic questions are politely refused.
- Secret Mission: Add conversation memory so the chatbot remembers previous messages and supports natural follow-up conversations.
Are there any prerequisites?
This project assumes you have Java 21, MySQL, and IntelliJ IDEA installed on your machine. You also need a Google account to get a free Gemini API key (no credit card required).
Before We Start
Before writing any code, let's lock in what you're about to build and why it matters. You're creating a role-based AI chatbot that bridges a MySQL database and Google Gemini, so every line of code from here on serves that goal.
Set Up the Project and Connect to Gemini
You are building an AI chatbot that connects Spring Boot, MySQL, and Google Gemini. Before you can write any feature code, you need a solid foundation with every dependency wired together correctly.
Getting this foundation wrong is the most common reason Spring AI projects fail on the first run. In this step, you will generate a Spring Boot project, add the AI and security libraries, connect to your database, and confirm everything starts cleanly.
In this step, get ready to:
- Generate a Spring Boot project with the required starter dependencies.
- Add Spring AI and JJWT dependencies to the project.
- Create the MySQL database and configure the application to connect to Gemini.
Generate the Spring Boot project
Spring Initializr is a web tool that scaffolds a ready-to-run Maven project with the dependencies you choose. You will use it to create the base project, then add the AI and security libraries manually.
- Open your browser and navigate to start.spring.io.
- Under Project, select Maven.
- Under Language, select Java.
- Under Spring Boot, select 3.5.16.
- Under Project Metadata, set Group to com.inventory and Artifact to chatbot.
- Set Name to inventory-chatbot.
- Under Java, select 21.
- Click Add Dependencies and search for each of these one at a time: Spring Web, Spring Data JPA, Spring Security, MySQL Driver, and Lombok.
- Click Generate to download a zip file.
- Extract the zip to your Desktop.
What did Spring Initializr just create?
The zip contains a complete Maven project with a pom.xml (your dependency list), a main application class, and an empty application.properties file. It also includes a Maven wrapper (mvnw) so you can build and run without installing Maven separately.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type IntelliJ IDEA and press Enter to open it.
- In IntelliJ, click Open on the welcome screen.
- Navigate to your Desktop and select the chatbot folder you just extracted.
- Click Open.
IntelliJ loads the project and imports the Maven dependencies automatically. You should see the project file tree in the left sidebar with folders like src/main/java and src/main/resources.
IntelliJ not showing the project files?
If IntelliJ asks whether to trust the project, click Trust Project. If the file tree is empty, right-click the project root in the sidebar and select Reload from Disk.
Help me troubleshoot IntelliJ not loading my Spring Boot project files.
Add Spring AI and JJWT dependencies
The project from Spring Initializr does not include Spring AI or the JWT library. You need to add these manually to pom.xml so your chatbot can talk to Gemini and authenticate users.
- In IntelliJ's file tree, double-click pom.xml to open it.
- Find the closing </properties> tag. Add the following <dependencyManagement> section directly below it:
<!-- Spring AI version management -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
What is a BOM?
A Bill of Materials (BOM) locks all Spring AI dependency versions to a single compatible set. By importing the spring-ai-bom at version 1.1.7, you can add any Spring AI starter below without specifying its version. The BOM handles that for you.
- Scroll down to the <dependencies> section. After the Lombok dependency, add the Spring AI and JJWT dependencies:
<!-- Spring AI Google Gemini integration -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
<!-- JWT token library: API, implementation, JSON serializer -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
What do these dependencies do?
The spring-ai-starter-model-google-genai starter auto-configures Spring AI to connect to Google Gemini. It gives you a ChatClient bean that sends prompts and receives responses with just a few lines of code.
The three JJWT artifacts (jjwt-api, jjwt-impl, jjwt-jackson) handle JWT token creation, signing, and parsing. You will use them in a later step to build authentication.
- Save pom.xml.
- IntelliJ shows a small Maven reload icon in the top right of the editor. Click the reload icon (or press Cmd+Shift+I on macOS, Ctrl+Shift+O on Windows) to import the new dependencies.
IntelliJ downloads the dependencies. When it finishes, the dependency names in pom.xml should have no red underlines.
Seeing red underlines in pom.xml?
If spring-ai-starter-model-google-genai shows an error, make sure the <dependencyManagement> block with the Spring AI BOM is placed between </properties> and <dependencies>. If JJWT dependencies are red, double-check the version is exactly 0.13.0.
Help me fix red underlines in my Spring Boot pom.xml dependencies.
✔️ Awesome, I've got everything!
Great. Double check you have saved pom.xml and that IntelliJ has finished reloading Maven.
ⓧ I'd like to double check the full code
Since pom.xml is a long file, scroll through it and confirm these sections are present in order:
- A <parent> block referencing spring-boot-starter-parent version 3.5.16.
- A <properties> block with <java.version>21</java.version>.
- A <dependencyManagement> block importing spring-ai-bom version 1.1.7.
- Dependencies for Spring Web, Spring Data JPA, Spring Security, spring-ai-starter-model-google-genai, MySQL Driver, Lombok, jjwt-api, jjwt-impl, and jjwt-jackson all at version 0.13.0.
- A <build> section with the spring-boot-maven-plugin and a Lombok exclusion.
Create the database and configure the application
Your Spring Boot app needs a MySQL database to store inventory data. You also need to point the app at your Gemini API key so Spring AI can connect.
- Open a terminal. In IntelliJ, click Terminal at the bottom of the window.
- Log in to MySQL by running this command:
mysql -u root -p
- Enter your MySQL root password when prompted.
- Create the project database by running:
CREATE DATABASE inventory_chatbot;
- Verify the database was created by running:
SHOW DATABASES;
You should see inventory_chatbot in the list of databases.
- Exit the MySQL shell by running:
exit;
Now you will configure the application to connect to this database and to Gemini. Spring Boot uses a YAML configuration file for this.
- In IntelliJ's file tree, expand src/main/resources.
- Right-click application.properties and select Refactor, then Rename.
- Rename it to application.yml and press Enter.
- Open the newly renamed application.yml and paste the following configuration:
spring:
# MySQL connection settings
datasource:
url: jdbc:mysql://localhost:3306/inventory_chatbot
username: root
password: your-mysql-password-here
driver-class-name: com.mysql.cj.jdbc.Driver
# Hibernate ORM settings
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
# Google Gemini AI configuration
ai:
google:
genai:
api-key: your-gemini-api-key-here
chat:
model: gemini-2.5-flash
temperature: 0.7
# JWT authentication settings
app:
jwt:
secret: Y2hhbmdlLXRoaXMtdG8tYS1sb25nLXNlY3VyZS1yYW5kb20tc3RyaW5n
expiration-ms: 3600000
What does this configuration do?
The datasource section tells Spring Data JPA how to reach your MySQL database. The ddl-auto: update setting automatically creates database tables from your Java entities when the app starts.
The spring.ai.google.genai section configures Spring AI to use the gemini-2.5-flash model with a temperature of 0.7 (higher means more creative responses). The app.jwt section stores the signing key and token lifetime for authentication you will build later.
- Replace your-mysql-password-here with your actual MySQL root password.
- Replace your-gemini-api-key-here with the Gemini API key you already have from Google AI Studio.
- Save application.yml.
Keep your API key safe
Never commit your API key or database password to a public repository. If you plan to push this project to GitHub later, add application.yml to your .gitignore file.
✔️ Awesome, I've got everything!
Great. Double check you have saved application.yml with your real MySQL password and Gemini API key filled in.
ⓧ I'd like to double check the full code
Your application.yml should look like this (with your actual credentials in place of the placeholders):
spring:
datasource:
url: jdbc:mysql://localhost:3306/inventory_chatbot
username: root
password: your-mysql-password-here
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
ai:
google:
genai:
api-key: your-gemini-api-key-here
chat:
model: gemini-2.5-flash
temperature: 0.7
app:
jwt:
secret: Y2hhbmdlLXRoaXMtdG8tYS1sb25nLXNlY3VyZS1yYW5kb20tc3RyaW5n
expiration-ms: 3600000
Before you run the application, what do you think will happen? Will it start cleanly, or will Spring Security do something unexpected?
- In IntelliJ's Terminal panel at the bottom, run the application using the Maven wrapper:
macOS
./mvnw spring-boot:run
Windows
mvnw.cmd spring-boot:run
The first run downloads dependencies and compiles the project, which can take a minute or two. When it finishes, you should see a line containing Started InventoryChatbotApplication in the console output.
Why does the console mention a generated security password?
Spring Security is active because you added the spring-boot-starter-security dependency. By default, it locks every endpoint and generates a random password printed in the console. This is expected. You will replace this default security with your own JWT-based authentication in Step 4.
Application fails to start?
If you see a connection error mentioning MySQL, check that your MySQL server is running and that the password in application.yml matches your actual root password.
If you see an error about the Gemini API key, make sure you pasted the full key value (no extra spaces or quotes) under spring.ai.google.genai.api-key in application.yml.
Help me debug why my Spring Boot application is failing to start.
- Press Ctrl+C in the terminal to stop the application. You will restart it in the next step.
Your foundation is solid. The project compiles, connects to MySQL, and Spring AI is wired to Gemini. Next up, you will build your first chat endpoint and prove that Gemini can actually respond through your backend.
Get Gemini Talking
Your Spring Boot project compiles, the database connects, and the Gemini API key is configured. There is just one problem: Spring Security is blocking every request with a default login page, so you cannot reach any endpoint you create.
Before layering on users, roles, or inventory data, you need proof that your backend can actually talk to Gemini. This step is your "it works" moment. You will temporarily disable security, wire up a single chat endpoint, and send your first message to Google Gemini through Spring Boot.
In this step, get ready to:
- Disable Spring Security so you can test endpoints freely.
- Create a chat endpoint that forwards messages to Gemini.
- Verify the AI responds to your first message via curl.
Disable Spring Security temporarily
Spring Security's default behavior locks down every endpoint behind an auto-generated password. You need to replace that behavior with a configuration that lets all requests through so you can test freely.
- In IntelliJ's Project panel, right-click on the com.inventory.chatbot package.
- Select New > Java Class.
- Type config.SecurityConfig and press Enter. IntelliJ creates both the config package and the file.
- Replace the generated file contents with this code:
package com.inventory.chatbot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Disable CSRF and permit all requests for testing
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}
What does this code do?
This is a temporary security configuration that you will replace in Step 4 with proper JWT authentication.
- .csrf(AbstractHttpConfigurer::disable) turns off CSRF protection. This is safe for a stateless REST API that uses tokens instead of cookies.
- .anyRequest().permitAll() lets every HTTP request through without authentication, so you can test your chat endpoint immediately.
- Save SecurityConfig.java.
- Stop the running application by clicking the red Stop button in IntelliJ's toolbar.
- Restart the application by clicking the green Run button.
The console should print Started InventoryChatbotApplication with no errors. The auto-generated security password message is gone because your custom config replaced the default.
Application won't start?
- Check that SecurityConfig.java is inside the com.inventory.chatbot.config package. Spring Boot only scans subpackages of your main application class.
- Verify the file compiles with no red underlines in IntelliJ. A missing import usually means the class name was mistyped.
Help me debug my SecurityConfig class not being picked up by Spring Boot.
✔️ Awesome, I've got everything!
Great. Double check you have saved SecurityConfig.java and the app restarts cleanly.
ⓧ I'd like to double check the full code
package com.inventory.chatbot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Disable CSRF and permit all requests for testing
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}
Create the chat endpoint
Spring AI auto-configures a ChatClient.Builder bean based on your application.yml settings. You inject this builder into your controller, call .build(), and you have a ready-to-use client that can send prompts to Gemini and return responses.
The controller needs a simple DTO to deserialize incoming JSON messages. You will create that first, then the controller itself.
- Right-click on com.inventory.chatbot in the Project panel.
- Select New > Java Class.
- Type dto.ChatRequest and select Record from the dropdown.
- Replace the generated file contents with this code:
package com.inventory.chatbot.dto;
// Holds the incoming chat message from the client
public record ChatRequest(String message) {}
- Save ChatRequest.java.
- Now create the controller. Right-click on com.inventory.chatbot again and select New > Java Class.
- Type controller.ChatController and press Enter.
- Replace the generated file contents with this code:
package com.inventory.chatbot.controller;
import com.inventory.chatbot.dto.ChatRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class ChatController {
// ChatClient handles communication with Gemini
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
// Spring AI auto-configures the Builder with your Gemini settings
this.chatClient = builder.build();
}
@PostMapping("/chat")
public ResponseEntity<?> chat(@RequestBody ChatRequest request) {
// Forward the user's message to Gemini and return the AI response
String response = chatClient.prompt()
.user(request.message())
.call()
.content();
return ResponseEntity.ok(Map.of("reply", response));
}
}
What does this code do?
ChatClient.Builder is auto-configured by Spring AI using the Gemini API key and model name from your application.yml. You call .build() in the constructor to create a reusable ChatClient instance.
The .prompt().user(message).call().content() chain creates a prompt, sets the user message, sends it to Gemini, and extracts the text response. The reply is wrapped in a JSON map so the response looks like {"reply": "..."}.
- Save ChatController.java.
- Stop the running application by clicking the red Stop button.
- Restart the application by clicking the green Run button.
The console should print Started InventoryChatbotApplication again with no errors. Your new /api/chat endpoint is now live.
Seeing a compilation error?
- If IntelliJ shows a red underline on ChatClient, check that the spring-ai-starter-model-google-genai dependency is in your pom.xml and reload Maven by clicking the Reload All Maven Projects button in the Maven panel.
- If ChatRequest cannot be resolved, verify the package declaration in ChatRequest.java is com.inventory.chatbot.dto.
Help me fix a compilation error in my ChatController.
✔️ Awesome, I've got everything!
Great. Make sure both ChatRequest.java and ChatController.java are saved and the app has restarted cleanly.
ⓧ I'd like to double check the full code
Here is ChatRequest.java:
package com.inventory.chatbot.dto;
// Holds the incoming chat message from the client
public record ChatRequest(String message) {}
Here is ChatController.java:
package com.inventory.chatbot.controller;
import com.inventory.chatbot.dto.ChatRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class ChatController {
// ChatClient handles communication with Gemini
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
// Spring AI auto-configures the Builder with your Gemini settings
this.chatClient = builder.build();
}
@PostMapping("/chat")
public ResponseEntity<?> chat(@RequestBody ChatRequest request) {
// Forward the user's message to Gemini and return the AI response
String response = chatClient.prompt()
.user(request.message())
.call()
.content();
return ResponseEntity.ok(Map.of("reply", response));
}
}
Talk to Gemini
Everything is wired up. Your Spring Boot backend has an open endpoint connected to Gemini through Spring AI. Time to see if it actually works.
Before you run the next command, predict: will your backend successfully send the message to Gemini and return an AI-generated reply?
- Open a terminal. In IntelliJ, click Terminal at the bottom of the window to open the built-in terminal.
- Send a message to Gemini through your backend by running this command:
curl -X POST http://localhost:8080/api/chat -H "Content-Type: application/json" -d "{\"message\":\"Hello, who are you?\"}"
You should see a JSON response containing Gemini's reply. It will look something like {"reply":"Hello! I'm a large language model..."}. The exact wording varies each time because Gemini generates a fresh response.
What just happened?
Your terminal sent an HTTP POST to your Spring Boot backend. The ChatController received the message, passed it to ChatClient, and ChatClient forwarded it to Google's Gemini API over the internet.
Gemini processed the prompt and returned a conversational response. Your backend wrapped that response in JSON and sent it back to your terminal. You are talking to the AI through your own server.
Not getting a response?
- If you see Connection refused, your Spring Boot application is not running. Check the IntelliJ console for errors and restart it.
- If you see a 403 or 401 error, your SecurityConfig may not have loaded. Verify the file is inside the com.inventory.chatbot.config package and the app restarted after you saved it.
- If you see an API key error from Gemini, double check the spring.ai.google.genai.api-key value in application.yml matches the key from Google AI Studio.
- On Windows PowerShell, curl is an alias for a different command. Use curl.exe instead (replace curl with curl.exe at the start of the command).
Help me debug why my Spring AI ChatClient is not connecting to Gemini.
You just proved that your Spring Boot backend can reach Gemini and return real AI responses. Right now, Gemini is answering with generic knowledge because it has no idea what your inventory looks like. Next up, you will build the data layer so your database has products and orders for the chatbot to talk about.
Build the Inventory Data Layer
You proved in the last step that your Spring Boot backend can talk to Gemini and get real responses. But here is the problem: Gemini is a language model running on Google's servers. It has zero access to your MySQL database.
To make the chatbot useful, you need actual data for the backend to query. In this step, you will create JPA entities that map to MySQL tables, build repositories that query those tables, and seed everything with sample inventory data.
In this step, get ready to:
- Create JPA entities for users, products, and orders.
- Build Spring Data JPA repositories with custom query methods.
- Seed the database with sample inventory data on startup.
Create the JPA entities
JPA entities are Java classes that map directly to MySQL tables. Hibernate, the JPA implementation included with Spring Boot, creates these tables automatically when the application starts.
- In IntelliJ's Project panel, right-click on src/main/java/com/inventory/chatbot, select New then Package, and name it model.
- Inside the model package, create a new Java file called Role.java by pasting this code:
package com.inventory.chatbot.model;
// Defines the two access levels for the chatbot
public enum Role {
ADMIN, CUSTOMER
}
- In the same model package, create a new Java file called User.java by pasting this code:
package com.inventory.chatbot.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// Maps to the "app_user" table to avoid the SQL reserved word "user"
@Entity
@Table(name = "app_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String name;
// Stores the role as a string (ADMIN or CUSTOMER) in the database
@Enumerated(EnumType.STRING)
private Role role;
}
What does this code do?
The Role enum defines two access levels: ADMIN and CUSTOMER. The chatbot will use these later to decide what data to show.
The User entity maps to a table called app_user because user is a reserved word in SQL. The @Enumerated(EnumType.STRING) annotation stores the role as readable text rather than an integer.
- Save both files.
- Stop the running application by clicking the red square in IntelliJ's Run tool window.
- Restart the application by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
- Open a new terminal tab and connect to MySQL by running:
mysql -u root -p
- Enter your MySQL password when prompted.
- Switch to your project database and list the tables by running:
USE inventory_chatbot;
SHOW TABLES;
You should see app_user in the list. Hibernate created the table from your User entity.
Don't see the app_user table?
- Check that spring.jpa.hibernate.ddl-auto is set to update in application.yml. This tells Hibernate to create tables automatically.
- Verify there are no red errors in the IntelliJ console. A missing import or typo prevents the app from starting.
- Make sure you ran USE inventory_chatbot; before SHOW TABLES;. Without switching databases, MySQL shows tables from the wrong schema.
Help me troubleshoot why Hibernate is not creating my app_user table in MySQL.
- Now create Product.java in the model package by pasting this code:
package com.inventory.chatbot.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String category;
private double price;
// Tracks current inventory level for low-stock alerts
private int stockQuantity;
}
What does this code do?
Each Product row stores a name, category, price, and stock count. The stockQuantity field is what the chatbot will use later to flag low-stock items.
- Create CustomerOrder.java in the model package by pasting this code:
package com.inventory.chatbot.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
// Named CustomerOrder instead of Order to avoid the SQL reserved word
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CustomerOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Links this order to a specific user
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// Links this order to a specific product
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private int quantity;
private double totalPrice;
private LocalDate orderDate;
}
Why CustomerOrder instead of Order?
ORDER is a reserved word in SQL. Naming the class Order would cause Hibernate to generate a table name that clashes with that keyword. CustomerOrder avoids the conflict without needing extra table-name annotations.
The @ManyToOne annotations create foreign key relationships. Each order links to one user and one product.
- Save both files.
- Restart the application by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
- Switch back to your MySQL terminal and run:
SHOW TABLES;
You should now see app_user, product, and customer_order in the list. All three tables are ready for data.
Missing product or customer_order table?
- Check the IntelliJ console for compilation errors. A missing import in CustomerOrder.java (such as java.time.LocalDate) prevents the app from starting.
- Confirm that CustomerOrder.java is in the com.inventory.chatbot.model package. A file in the wrong package will not be scanned by Spring.
Help me figure out why Hibernate is not creating all my tables.
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm each file in the model package matches the code above. You should have exactly four files:
- Role.java with ADMIN and CUSTOMER values.
- User.java with @Table(name = "app_user") and a Role field.
- Product.java with name, category, price, and stockQuantity fields.
- CustomerOrder.java with @ManyToOne relationships to User and Product.
Create the Spring Data repositories
Spring Data JPA generates database queries from method names. You write an interface, declare a method like findByUsername(), and Spring builds the SQL for you.
- In the Project panel, right-click on src/main/java/com/inventory/chatbot, select New then Package, and name it repository.
- Inside the repository package, create UserRepository.java by pasting this code:
package com.inventory.chatbot.repository;
import com.inventory.chatbot.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data generates the query from the method name
Optional<User> findByUsername(String username);
}
- Create ProductRepository.java in the same repository package by pasting this code:
package com.inventory.chatbot.repository;
import com.inventory.chatbot.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Finds products with stock below the given threshold
List<Product> findByStockQuantityLessThan(int threshold);
}
What do these repositories do?
UserRepository provides findByUsername() to look up users during login. ProductRepository provides findByStockQuantityLessThan() to find low-stock products. Spring Data auto-generates the SQL from these method names.
- Save both files.
- Restart the application by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
- Confirm the IntelliJ console still shows "Started InventoryChatbotApplication" with no errors.
Seeing a compilation error?
- Make sure each repository interface is in the com.inventory.chatbot.repository package. The package declaration at the top of the file must match the folder path.
- Verify the import for User and Product points to com.inventory.chatbot.model, not a different package.
Help me fix a compilation error in my Spring Data repository.
- Create the last repository, OrderRepository.java, in the repository package by pasting this code:
package com.inventory.chatbot.repository;
import com.inventory.chatbot.model.CustomerOrder;
import com.inventory.chatbot.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface OrderRepository extends JpaRepository<CustomerOrder, Long> {
// Retrieves all orders placed by a specific user
List<CustomerOrder> findByUser(User user);
}
What does findByUser do?
This method generates a query that filters orders by the user foreign key. When a customer logs in, the chatbot will call findByUser() to retrieve only that customer's orders.
✔️ Awesome, I've got everything!
Great. Save OrderRepository.java before moving on.
ⓧ I'd like to double check the full code
Confirm you have three files in the repository package:
- UserRepository.java with findByUsername(String username).
- ProductRepository.java with findByStockQuantityLessThan(int threshold).
- OrderRepository.java with findByUser(User user).
Seed the database with sample data
Your tables exist but they are empty. Spring Boot's CommandLineRunner interface runs code automatically on startup. You will use it to seed users, products, and orders every time the app starts fresh.
The data seeder needs to hash passwords. That requires a PasswordEncoder bean, which does not exist yet. Let's add one to SecurityConfig first.
- Open SecurityConfig.java in the config package.
- Add these two imports below the existing imports at the top of the file:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
- Below the securityFilterChain method's closing brace, add this new bean method:
// Provides password hashing for user credentials
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
What is BCryptPasswordEncoder?
BCrypt is a one-way hashing algorithm designed for passwords. It automatically adds a random salt to each hash, so even identical passwords produce different hashes. Spring Security uses this bean whenever it needs to encode or verify a password.
- Save SecurityConfig.java.
- Now create DataLoader.java in the config package by pasting this code:
package com.inventory.chatbot.config;
import com.inventory.chatbot.model.*;
import com.inventory.chatbot.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component
@RequiredArgsConstructor
public class DataLoader implements CommandLineRunner {
private final UserRepository userRepository;
private final ProductRepository productRepository;
private final OrderRepository orderRepository;
private final PasswordEncoder passwordEncoder;
@Override
public void run(String... args) {
// Skip seeding if data already exists
if (userRepository.count() > 0) return;
// Create two users with hashed passwords
User admin = userRepository.save(new User(null, "admin", passwordEncoder.encode("admin123"), "Alice Admin", Role.ADMIN));
User customer = userRepository.save(new User(null, "customer", passwordEncoder.encode("cust123"), "Bob Customer", Role.CUSTOMER));
}
}
What does this code do?
The @Component annotation registers this class with Spring. CommandLineRunner tells Spring to call the run() method automatically when the app starts.
The guard clause if (userRepository.count() > 0) return prevents duplicate data on repeated restarts. Passwords are hashed with BCrypt before saving.
- Save DataLoader.java.
- Restart the application by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
- Switch back to your MySQL terminal and run this query:
SELECT id, username, name, role FROM app_user;
You should see two rows: admin with role ADMIN and customer with role CUSTOMER.
No rows in app_user?
- Check that the IntelliJ console shows "Started InventoryChatbotApplication" with no errors. A missing PasswordEncoder bean causes a startup failure.
- Verify the two new imports (BCryptPasswordEncoder and PasswordEncoder) are present in SecurityConfig.java.
- Confirm DataLoader.java is in the com.inventory.chatbot.config package. Spring only scans packages under the main application class.
Help me figure out why DataLoader is not seeding data on startup.
Now add the product and order data. The tables exist but are still empty.
- In DataLoader.java, find the line that creates the customer user. Copy the following code below it, before the closing } of the run method:
// Create 5 sample products with varying stock levels
Product p1 = productRepository.save(new Product(null, "Wireless Mouse", "Electronics", 29.99, 150));
Product p2 = productRepository.save(new Product(null, "Mechanical Keyboard", "Electronics", 89.99, 3));
Product p3 = productRepository.save(new Product(null, "USB-C Hub", "Accessories", 45.50, 8));
Product p4 = productRepository.save(new Product(null, "Monitor Stand", "Furniture", 65.00, 42));
Product p5 = productRepository.save(new Product(null, "Webcam HD", "Electronics", 55.00, 2));
// Create 3 orders linked to the customer
orderRepository.save(new CustomerOrder(null, customer, p1, 2, 59.98, LocalDate.now().minusDays(5)));
orderRepository.save(new CustomerOrder(null, customer, p3, 1, 45.50, LocalDate.now().minusDays(2)));
orderRepository.save(new CustomerOrder(null, customer, p4, 1, 65.00, LocalDate.now().minusDays(1)));
What does this seed data set up?
Five products are created with deliberately varied stock levels. The Mechanical Keyboard (3 units) and Webcam HD (2 units) are low-stock items that the chatbot will flag later when an admin asks about inventory health.
Three orders are linked to the customer user. When the customer asks about their purchases, the chatbot will only see these three orders. The admin will see everything.
- Save DataLoader.java.
- The data seeder will re-run because the guard clause checks userRepository.count(). Since users already exist from the last run, delete the existing data first by running this in your MySQL terminal:
DELETE FROM customer_order;
DELETE FROM app_user;
DELETE FROM product;
- Restart the application by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
Before you check the results, what do you expect to see? Think about how many products and how many orders the DataLoader creates.
- Run these queries in your MySQL terminal to verify the seed data:
SELECT * FROM product;
SELECT * FROM customer_order;
SELECT * FROM product returns 5 rows. SELECT * FROM customer_order returns 3 rows. The data layer is ready.
Seeing fewer rows than expected?
- If products or orders are missing, check that you pasted the new code below the customer line and above the closing } of the run method.
- If the guard clause skipped seeding (because users already existed), delete all data with the queries above and restart again.
- Check that customer_order rows reference valid product IDs. If product rows were not inserted first, the foreign key constraint will reject orders.
Help me troubleshoot missing seed data in my Spring Boot DataLoader.
✔️ Awesome, I've got everything!
Great. Confirm you have saved both SecurityConfig.java and DataLoader.java.
ⓧ I'd like to double check the full code
Here is the full SecurityConfig.java with the PasswordEncoder bean added:
package com.inventory.chatbot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
For DataLoader.java, scroll up and confirm your file has: the guard clause, 2 users, 5 products, and 3 orders inside the run method. The file should start with the class declaration and end with the closing brace.
Your data layer is complete. You have entities, repositories, and real data sitting in MySQL. Next up, you will lock all of this down with JWT authentication so the backend knows who is asking the questions.
Lock It Down with JWT Authentication
Your data layer is built and seeded with users, products, and orders. But there is a gaping hole in your application.
Anyone can hit your /api/chat endpoint without identifying themselves. The backend has no idea who is asking, which means it cannot serve different data to different roles. JWT authentication fixes this by requiring a signed token on every request that proves who the caller is.
In this step, get ready to:
- Create a JWT service that signs and validates tokens using JJWT.
- Build a login endpoint that authenticates users and returns JWTs.
- Replace the temporary security config with production rules that require authentication.
Create the JWT token service
A JWT (JSON Web Token) contains claims like the username and role, signed with a secret key. Your backend verifies the signature on every request to confirm the token has not been tampered with.
- In IntelliJ IDEA, right-click the com.inventory.chatbot.security package in the Project tool window.
- Select New, then Java Class.
- Name it JwtService and press Enter.
- Delete everything IntelliJ generated in the file and paste this first section:
package com.inventory.chatbot.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
@Service
public class JwtService {
// Read the JWT signing secret and expiration from application.yml
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration-ms}")
private long expirationMs;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
What does this code do?
The @Value annotations pull the secret key and token lifetime from your application.yml.
The getSigningKey() method decodes your Base64 secret into an HS256-compatible HMAC key that JJWT uses to sign and verify tokens.
- Paste the token creation and extraction methods directly below getSigningKey():
// Build a signed JWT with username and role as claims
public String generateToken(String username, String role) {
return Jwts.builder()
.subject(username)
.claim("role", role)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(getSigningKey())
.compact();
}
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
public String extractRole(String token) {
return extractClaims(token).get("role", String.class);
}
What does this code do?
The generateToken() method builds a JWT with the username as the subject and the role as a custom claim, then signs it with your HMAC key.
The extractUsername() and extractRole() methods parse the token and pull out those values.
- Complete the file by pasting the validation methods and the closing brace below extractRole():
// Validate by attempting to parse; invalid tokens throw exceptions
public boolean isTokenValid(String token) {
try {
extractClaims(token);
return true;
} catch (Exception e) {
return false;
}
}
// Parse and verify the token's signature, then return its claims
private Claims extractClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
}
What does this code do?
The isTokenValid() method tries to parse the token. If parsing succeeds, the token is valid. If the signature is wrong or the token is expired, JJWT throws an exception and the method returns false.
The extractClaims() method is the private workhorse that verifies the signature and returns the token's payload.
✔️ Awesome, I've got everything!
Save JwtService.java.
ⓧ I'd like to double check the full code
Your JwtService.java should contain these sections in order:
- The class declaration with @Value fields and getSigningKey() (from the first code block above).
- The generateToken(), extractUsername(), and extractRole() methods (from the second code block).
- The isTokenValid() and extractClaims() methods, plus the closing } (from the third code block).
Scroll up and confirm each section is present and in the right order.
- Restart your application by pressing Ctrl+F5 (Windows/Linux) or clicking the Rerun button in the Run tool window.
- You should see Started InventoryChatbotApplication in the console with no errors.
Seeing a dependency error?
- Make sure your pom.xml includes all three JJWT dependencies: jjwt-api, jjwt-impl, and jjwt-jackson.
- Confirm jjwt-impl and jjwt-jackson have <scope>runtime</scope> set.
- If IntelliJ shows red underlines, click Load Maven Changes in the notification bar or press Ctrl+Shift+O (Windows/Linux).
Still stuck? Help me fix my JJWT dependency error in my Spring Boot project.
Build the login endpoint
Your backend needs a way to verify a username and password, then hand back a JWT. This requires two small DTO records, a Spring Security UserDetailsService, and an authentication controller.
- Right-click the com.inventory.chatbot.dto package, select New, then Java Class.
- Name it LoginRequest, select Record as the type, and press Enter.
- Replace the file contents with:
package com.inventory.chatbot.dto;
// Captures the username and password from the login request body
public record LoginRequest(String username, String password) {}
- Create another record in the same dto package named LoginResponse with these contents:
package com.inventory.chatbot.dto;
// Returns the JWT token and the user's role after successful login
public record LoginResponse(String token, String role) {}
- Save both files.
Spring Security needs a way to look up users from your database. You will create a service that bridges your UserRepository and Spring Security's UserDetailsService interface.
- Right-click the com.inventory.chatbot.security package and create a new Java class named CustomUserDetailsService.
- Replace the file contents with:
package com.inventory.chatbot.security;
import com.inventory.chatbot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Look up the user in MySQL and convert to a Spring Security UserDetails
var user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))
);
}
}
What does this code do?
Spring Security calls loadUserByUsername() whenever it needs to verify a user. This method queries your UserRepository and wraps the result in a UserDetails object with the correct role authority.
- Save CustomUserDetailsService.java.
- Restart the application with Ctrl+F5 to confirm it still starts successfully.
Now create the controller that accepts login credentials and returns a JWT.
- Right-click the com.inventory.chatbot.controller package and create a new Java class named AuthController.
- Replace the file contents with:
package com.inventory.chatbot.controller;
import com.inventory.chatbot.dto.*;
import com.inventory.chatbot.model.User;
import com.inventory.chatbot.repository.UserRepository;
import com.inventory.chatbot.security.JwtService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserRepository userRepository;
private final JwtService jwtService;
private final PasswordEncoder passwordEncoder;
// Authenticate credentials and return a JWT
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
User user = userRepository.findByUsername(request.username())
.orElse(null);
if (user == null || !passwordEncoder.matches(request.password(), user.getPassword())) {
return ResponseEntity.status(401).body("Invalid credentials");
}
String token = jwtService.generateToken(user.getUsername(), user.getRole().name());
return ResponseEntity.ok(new LoginResponse(token, user.getRole().name()));
}
}
What does this code do?
The POST /api/auth/login endpoint looks up the user in the database, verifies the password using BCrypt, and returns a signed JWT containing the username and role. If the credentials are wrong, it returns a 401 response.
- Save AuthController.java and restart the application.
The login endpoint is live. Because the temporary SecurityConfig still permits all requests, you can test it immediately.
- Open the Terminal tab at the bottom of IntelliJ IDEA and test the login endpoint by running:
curl -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"admin123\"}"
You should see a JSON response containing a token field (a long encoded string) and "role":"ADMIN".
Curl on Windows
The curl command above uses escaped double quotes which work in both Windows Command Prompt and macOS/Linux terminals. If you are using IntelliJ's built-in terminal, it should work as-is. If you get a quoting error in PowerShell, run cmd first to switch to Command Prompt, then retry the curl command.
Getting 401 Invalid credentials?
- Check that your DataLoader uses admin123 as the admin password. The password must match exactly.
- If you have run the app before, the database already has users from a previous run. Drop and recreate the inventory_chatbot database in MySQL, then restart the app so DataLoader seeds fresh data.
Still stuck? Help me debug my Spring Boot login endpoint returning 401.
Wire the authentication filter and lock down security
Login works, but every endpoint is still wide open. You need a filter that intercepts every HTTP request, extracts the JWT from the Authorization header, validates it, and tells Spring Security who the caller is.
- Create a new Java class in the com.inventory.chatbot.security package named JwtAuthenticationFilter.
- Replace the file contents with this first section:
package com.inventory.chatbot.security;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
- Paste the method body directly below the opening brace of doFilterInternal:
// Check for a Bearer token in the Authorization header
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String token = authHeader.substring(7);
if (jwtService.isTokenValid(token)) {
String username = jwtService.extractUsername(token);
String role = jwtService.extractRole(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// Set the authenticated user in Spring Security's context
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null,
List.of(new SimpleGrantedAuthority("ROLE_" + role)));
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
What does this filter do?
Every HTTP request passes through this filter before reaching your controllers. It reads the Authorization: Bearer <token> header, validates the token with JwtService, and sets the authenticated user in Spring Security's context.
If the header is missing or the token is invalid, the filter passes the request along without authentication. Spring Security then decides whether to block it based on the security config.
✔️ Awesome, I've got everything!
Save JwtAuthenticationFilter.java.
ⓧ I'd like to double check the full code
Your JwtAuthenticationFilter.java should contain two sections in order:
- The class declaration with imports, fields, and the doFilterInternal method signature (from the first code block above).
- The method body with the header check, token validation, and SecurityContext setup (from the second code block).
Scroll up and confirm both sections are present and the braces are balanced.
Now replace the temporary security config with the production version that requires authentication on all endpoints except login.
- Open SecurityConfig.java in the com.inventory.chatbot.config package.
- Delete all the code in the file and paste this first section:
package com.inventory.chatbot.config;
import com.inventory.chatbot.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
- Paste the bean methods directly below the field declaration:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
// Use stateless sessions since JWTs handle authentication
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/index.html", "/css/**", "/js/**", "/*.ico").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
What changed in SecurityConfig?
The old config permitted every request. The new config does three critical things.
- Sets sessions to STATELESS so the server never stores session data. Every request must carry its own JWT.
- Allows /api/auth/** and static resources without a token. All other endpoints require authentication.
- Registers your JwtAuthenticationFilter to run before Spring Security's default username/password filter.
✔️ Awesome, I've got everything!
Save SecurityConfig.java.
ⓧ I'd like to double check the full code
Your updated SecurityConfig.java should contain two sections in order:
- The class declaration with imports and the jwtAuthenticationFilter field (from the first code block above).
- Three @Bean methods: securityFilterChain, passwordEncoder, and authenticationManager (from the second code block).
Scroll up and confirm both sections are present and the braces are balanced.
- Restart the application with Ctrl+F5.
Before you test, what do you think will happen if you try to chat without a token? Take a moment to predict the response.
- Try hitting the chat endpoint without a token by running:
curl -X POST http://localhost:8080/api/chat -H "Content-Type: application/json" -d "{\"message\":\"Hello\"}"
You get a 401 Unauthorized response. The endpoint is now locked down.
- Log in as admin to get a token by running:
curl -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"admin123\"}"
- Copy the token value from the response (the long string between the quotes).
- Use that token to chat with Gemini by running this command, replacing your-token-here with the token you just copied:
curl -X POST http://localhost:8080/api/chat -H "Content-Type: application/json" -H "Authorization: Bearer your-token-here" -d "{\"message\":\"What are the total sales?\"}"
Gemini responds with a conversational answer. Authentication works.
- Now log in as the customer by running:
curl -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"customer\",\"password\":\"cust123\"}"
- Copy the customer's token and ask the same question:
curl -X POST http://localhost:8080/api/chat -H "Content-Type: application/json" -H "Authorization: Bearer your-token-here" -d "{\"message\":\"What are the total sales?\"}"
Compare the two responses. Both the admin and the customer get the same generic answer from Gemini. The AI does not mention any specific products, orders, or revenue numbers.
Why are both answers identical?
Authentication confirms WHO is asking. But your ChatController still forwards the user's message straight to Gemini without any database context. Gemini is a language model on Google's servers. It has zero access to your MySQL data, so it gives the same generic response to everyone.
Fixing this gap is the entire point of the next step. You will query the database for role-specific data and inject it directly into the Gemini prompt.
Not getting a 401 without a token?
- Confirm you replaced the entire SecurityConfig.java file. The old version had .anyRequest().permitAll() which allows everything. The new version must have .anyRequest().authenticated().
- Make sure you restarted the application after saving the changes. Java does not hot-reload.
Still stuck? Help me troubleshoot my Spring Security config.
Authentication is working. Every request to /api/chat now requires a valid JWT. But you have seen the gap: the AI gives identical generic answers regardless of who is logged in. In the next step, you will fix this by querying the database for role-specific inventory data and injecting it directly into the Gemini prompt.
Inject Database Context into Gemini by Role
Authentication works. Both admin and customer can log in, get a token, and talk to Gemini. But there is a glaring problem: Gemini gives the exact same generic answer to both users.
The fix is the data bridge pattern. Before calling Gemini, your backend queries MySQL for role-appropriate inventory data and injects it directly into the system prompt as context. Gemini turns raw numbers into conversational answers, but it never touches your database.
In this step, get ready to:
- Create an InventoryService that queries MySQL and formats role-specific context strings.
- Build a ChatService that injects database context into the Gemini system prompt with an out-of-scope guard.
- Add a browser chat UI with login and authenticated messaging.
Create the InventoryService
Gemini is a language model on Google's servers. It has zero access to your MySQL database. Your backend needs to query the data, format it as plain text, and hand it to Gemini as context inside the prompt.
- In IntelliJ, right-click the com.inventory.chatbot.service package in the Project sidebar.
- Select New, then Java Class.
- Name it InventoryService and paste the following code:
package com.inventory.chatbot.service;
import com.inventory.chatbot.model.CustomerOrder;
import com.inventory.chatbot.model.Product;
import com.inventory.chatbot.model.User;
import com.inventory.chatbot.repository.OrderRepository;
import com.inventory.chatbot.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class InventoryService {
private final ProductRepository productRepository;
private final OrderRepository orderRepository;
// Builds a summary string of all inventory data for admin users
public String getAdminContext() {
List<Product> allProducts = productRepository.findAll();
List<CustomerOrder> allOrders = orderRepository.findAll();
List<Product> lowStock = productRepository.findByStockQuantityLessThan(10);
double totalRevenue = allOrders.stream().mapToDouble(CustomerOrder::getTotalPrice).sum();
StringBuilder sb = new StringBuilder();
sb.append("INVENTORY DATABASE CONTEXT (ADMIN VIEW):\n");
sb.append("Total products: ").append(allProducts.size()).append("\n");
sb.append("Total orders: ").append(allOrders.size()).append("\n");
sb.append("Total revenue: $").append(String.format("%.2f", totalRevenue)).append("\n");
}
}
What does this code do?
This service class acts as the bridge between your MySQL database and the AI prompt.
- productRepository.findAll() and orderRepository.findAll() pull every product and order from MySQL.
- findByStockQuantityLessThan(10) uses the query method you defined in ProductRepository to find products running low.
- The StringBuilder formats everything into a plain-text summary that Gemini can read as context.
The method is incomplete. It builds the top-level stats but does not list individual products yet. Add the rest of getAdminContext() and a second method for customers.
- In InventoryService.java, find this line inside getAdminContext():
sb.append("Total revenue: $").append(String.format("%.2f", totalRevenue)).append("\n");
}
- Replace the closing brace of the method with the following code so getAdminContext() also lists low-stock and all products:
sb.append("Total revenue: $").append(String.format("%.2f", totalRevenue)).append("\n");
sb.append("Low stock items (below 10 units):\n");
for (Product p : lowStock) {
sb.append(" - ").append(p.getName()).append(" (")
.append(p.getCategory()).append("): ")
.append(p.getStockQuantity()).append(" units, $")
.append(String.format("%.2f", p.getPrice())).append("\n");
}
sb.append("All products:\n");
for (Product p : allProducts) {
sb.append(" - ").append(p.getName()).append(": ")
.append(p.getStockQuantity()).append(" in stock, $")
.append(String.format("%.2f", p.getPrice())).append("\n");
}
return sb.toString();
}
What do the for loops do?
Each loop iterates over a list of products and appends a formatted line to the context string. The low-stock loop shows only items below 10 units. The all-products loop gives Gemini the complete picture so it can answer questions like "how much does the Webcam HD cost?" accurately.
That covers the admin view. Now the chatbot needs a customer-specific view that only shows that user's personal orders.
- Add the getCustomerContext() method below getAdminContext(), inside the InventoryService class:
// Builds a summary string of only this customer's order data
public String getCustomerContext(User user) {
List<CustomerOrder> orders = orderRepository.findByUser(user);
double totalSpent = orders.stream()
.mapToDouble(CustomerOrder::getTotalPrice).sum();
StringBuilder sb = new StringBuilder();
sb.append("CUSTOMER DATABASE CONTEXT (PERSONAL VIEW for ")
.append(user.getName()).append("):\n");
sb.append("Total orders: ").append(orders.size()).append("\n");
sb.append("Total spent: $")
.append(String.format("%.2f", totalSpent)).append("\n");
sb.append("Order history:\n");
for (CustomerOrder o : orders) {
sb.append(" - ").append(o.getProduct().getName())
.append(": qty ").append(o.getQuantity())
.append(", $").append(String.format("%.2f", o.getTotalPrice()))
.append(", ordered ").append(o.getOrderDate()).append("\n");
}
return sb.toString();
}
What does getCustomerContext do?
It takes a User object and queries only that user's orders via findByUser(user). The resulting context string contains the customer's name, their total spend, and their order history. Gemini never sees system-wide data for customer users.
- Save InventoryService.java.
- Restart your application in IntelliJ by clicking the green Rerun button in the Run panel.
- Confirm the console prints Started InventoryChatbotApplication with no errors. This proves the new service compiles and Spring can wire it.
Application fails to start?
- Check that InventoryService.java is in the com.inventory.chatbot.service package. A misplaced file will not be picked up by Spring's component scan.
- Confirm that both getAdminContext() and getCustomerContext() have matching opening and closing braces.
- Need more help?
✔️ Awesome, I've got everything!
Double check you have saved the file, then move on to the next substep.
ⓧ I'd like to double check the full code
Your InventoryService.java should contain three sections you built above. Scroll up and confirm each is present, in order:
- The class declaration with imports, fields, and the opening of getAdminContext() (stats section).
- The low-stock and all-products loops completing getAdminContext().
- The full getCustomerContext() method.
Build the role-aware ChatService and update ChatController
Right now, ChatController sends messages straight to Gemini with no context. You need a dedicated ChatService that builds a system prompt containing the inventory data from InventoryService plus rules that keep Gemini on-topic.
- Right-click the com.inventory.chatbot.service package, select New, then Java Class.
- Name it ChatService and paste the following code:
package com.inventory.chatbot.service;
import com.inventory.chatbot.model.Role;
import com.inventory.chatbot.model.User;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
private final InventoryService inventoryService;
// Build ChatClient from the auto-configured builder
public ChatService(ChatClient.Builder builder,
InventoryService inventoryService) {
this.chatClient = builder.build();
this.inventoryService = inventoryService;
}
}
Why an explicit constructor instead of @RequiredArgsConstructor?
The field type is ChatClient, but Spring auto-configures a ChatClient.Builder bean. The constructor accepts the builder and calls .build() to produce the client. Lombok cannot handle that transformation, so you write the constructor yourself.
The service exists but has no chat() method yet. This method is where the data bridge pattern comes together.
- Add the chat() method inside the ChatService class, below the constructor:
// Builds a role-specific system prompt and sends it to Gemini
public String chat(String userMessage, User user) {
// Pick the right context based on the user's role
String context = user.getRole() == Role.ADMIN
? inventoryService.getAdminContext()
: inventoryService.getCustomerContext(user);
// Build the system prompt with identity, rules, and data
String systemPrompt = """
You are an AI assistant for an inventory management system.
You help users understand their inventory data by answering questions in a clear, conversational tone.
IMPORTANT RULES:
- Only answer questions related to inventory, products, orders, and sales.
- If asked about anything outside inventory management, respond exactly: "This is outside my scope. I can only help with inventory-related questions."
- Base your answers ONLY on the data provided below. Do not make up data.
- If the user is a customer, you can only see their personal orders. Do not reference system-wide data.
%s
""".formatted(context);
return chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call()
.content();
}
What does the system prompt do?
The system prompt has three layers. First, it tells Gemini its identity as an inventory assistant. Second, it sets rules: stay on-topic, refuse off-topic questions, and never make up data. Third, it injects the database context via the %s placeholder.
Because context is chosen by the user's role, an admin gets system-wide product and sales data while a customer gets only their personal order history. Same question, different data.
- Save ChatService.java.
Now ChatController needs to stop calling Gemini directly and delegate to ChatService instead. It also needs to read the authenticated user from the JWT so it knows who is asking.
- Open ChatController.java in the controller package.
- Replace the entire contents of the file with the following code:
package com.inventory.chatbot.controller;
import com.inventory.chatbot.dto.ChatRequest;
import com.inventory.chatbot.model.User;
import com.inventory.chatbot.repository.UserRepository;
import com.inventory.chatbot.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
private final UserRepository userRepository;
// Extract the authenticated user and delegate to ChatService
@PostMapping("/chat")
public ResponseEntity<?> chat(@RequestBody ChatRequest request,
Authentication authentication) {
User user = userRepository.findByUsername(
authentication.getName()).orElseThrow();
String response = chatService.chat(request.message(), user);
return ResponseEntity.ok(Map.of("reply", response));
}
}
What changed in ChatController?
- The controller no longer builds a ChatClient itself. It injects ChatService and UserRepository instead.
- The chat() method now accepts an Authentication parameter. Spring Security injects this automatically from the JWT.
- authentication.getName() returns the username from the token. The controller loads the full User entity so ChatService can check their role.
- Save ChatController.java.
- Restart the application by clicking the green Rerun button.
- Wait for Started InventoryChatbotApplication in the console.
Time to prove the data bridge works. Before you run the test, think about this: if you log in as admin and ask about total sales, then log in as customer and ask the same question, do you think you will get the same answer or different answers?
- Log in as admin by running this command:
curl -s -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"admin123\"}"
- Copy the token value from the response.
- Ask the admin chatbot about total sales by running this command, replacing the token placeholder with your copied token:
curl -s -X POST http://localhost:8080/api/chat -H "Content-Type: application/json" -H "Authorization: Bearer your-admin-token-here" -d "{\"message\":\"What are the total sales?\"}"
Gemini responds with a system-wide sales summary including total revenue and product details. Now test the customer perspective.
- Log in as customer by running this command:
curl -s -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"customer\",\"password\":\"cust123\"}"
- Copy the customer token.
- Ask the same question using the customer token:
curl -s -X POST http://localhost:8080/api/chat -H "Content-Type: application/json" -H "Authorization: Bearer your-customer-token-here" -d "{\"message\":\"What are the total sales?\"}"
The customer response only mentions Bob Customer's personal orders. The same question, asked by two different users, produces completely different answers. That is the data bridge pattern in action.
Getting a 500 error or no inventory data in the response?
- Check the IntelliJ console for a stack trace. A NullPointerException in ChatController usually means authentication.getName() returned a username that does not match any user in the database.
- If the response mentions inventory but has no numbers, verify that your DataLoader ran. Query your database directly with SELECT * FROM product; to confirm 5 products exist.
- Need more help?
Add the chat interface
Curl proves the backend works, but your users will not be running terminal commands. A simple HTML page with a login form and chat window will let you demo the full experience in a browser.
- In IntelliJ's Project sidebar, right-click the src/main/resources/static folder.
- Select New, then File.
- Name it index.html.
Why does Spring Boot serve this file automatically?
Spring Boot serves any file placed in src/main/resources/static/ as a static resource. Your SecurityConfig already permits requests to /index.html, so the page loads without authentication.
- Paste the HTML head section with all CSS styles into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventory Chatbot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; height: 100vh; display: flex; justify-content: center; align-items: center; }
.container { width: 500px; background: white; border-radius: 12px; box-shadow: 0 2px 16px rgba(0,0,0,0.1); overflow: hidden; }
.header { background: #1a73e8; color: white; padding: 16px 20px; font-size: 18px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
.header button { background: rgba(255,255,255,0.2); border: none; color: white; padding: 6px 12px; border-radius: 6px; cursor: pointer; }
#login-form { padding: 40px 20px; }
#login-form input { width: 100%; padding: 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
#login-form button { width: 100%; padding: 12px; background: #1a73e8; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; }
#chat-area { display: none; }
#messages { height: 400px; overflow-y: auto; padding: 16px; }
.msg { margin-bottom: 12px; max-width: 80%; padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
.user-msg { background: #1a73e8; color: white; margin-left: auto; border-bottom-right-radius: 4px; }
.bot-msg { background: #e8eaed; color: #333; border-bottom-left-radius: 4px; }
.input-area { display: flex; padding: 12px; border-top: 1px solid #eee; }
.input-area input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
.input-area button { margin-left: 8px; padding: 10px 16px; background: #1a73e8; color: white; border: none; border-radius: 8px; cursor: pointer; }
.role-badge { font-size: 12px; background: rgba(255,255,255,0.3); padding: 2px 8px; border-radius: 10px; }
</style>
</head>
What do these styles do?
The CSS creates a centered card layout with a blue header, a login form that hides after authentication, and a chat area with distinct bubbles for user messages (blue, right-aligned) and bot messages (grey, left-aligned).
- Add the HTML body with the login form and chat interface below the </head> tag:
<body>
<div class="container">
<div class="header">
<span>Inventory Chatbot</span>
<span id="user-info"></span>
</div>
<div id="login-form">
<input type="text" id="username" placeholder="Username" value="admin">
<input type="password" id="password" placeholder="Password" value="admin123">
<button onclick="login()">Log In</button>
<p id="login-error" style="color:red;margin-top:8px;font-size:13px;"></p>
</div>
<div id="chat-area">
<div id="messages"></div>
<div class="input-area">
<input type="text" id="msg-input" placeholder="Ask about your inventory..." onkeydown="if(event.key==='Enter')sendMessage()">
<button onclick="sendMessage()">Send</button>
</div>
</div>
</div>
What does this HTML structure do?
- The login-form div shows username and password fields pre-filled with the admin credentials for quick testing.
- The chat-area div starts hidden (display: none in the CSS) and appears after a successful login.
- The message input listens for the Enter key so the user can send messages without clicking the button.
- Add the JavaScript that powers login and chat below the closing </div> of the container:
<script>
let token = '';
let role = '';
async function login() {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: document.getElementById('username').value, password: document.getElementById('password').value })
});
if (!res.ok) { document.getElementById('login-error').textContent = 'Invalid credentials'; return; }
const data = await res.json();
token = data.token; role = data.role;
document.getElementById('login-form').style.display = 'none';
document.getElementById('chat-area').style.display = 'block';
document.getElementById('user-info').innerHTML = '<span class="role-badge">' + role + '</span> <button onclick="logout()">Logout</button>';
addMessage('Hello! I am your inventory assistant. Ask me anything about your inventory.', 'bot');
}
function logout() { token = ''; location.reload(); }
async function sendMessage() {
const input = document.getElementById('msg-input');
const msg = input.value.trim(); if (!msg) return;
input.value = '';
addMessage(msg, 'user');
addMessage('Thinking...', 'bot');
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ message: msg })
});
const msgs = document.getElementById('messages');
msgs.removeChild(msgs.lastChild);
if (res.ok) { const data = await res.json(); addMessage(data.reply, 'bot'); }
else { addMessage('Error: Could not get response.', 'bot'); }
}
function addMessage(text, type) {
const div = document.createElement('div');
div.className = 'msg ' + (type === 'user' ? 'user-msg' : 'bot-msg');
div.textContent = text;
const msgs = document.getElementById('messages');
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
}
</script>
</body>
</html>
What do these functions do?
- login() sends credentials to /api/auth/login, stores the JWT token, hides the login form, and shows the chat area.
- sendMessage() posts the user's message to /api/chat with the Bearer token in the Authorization header. It shows a "Thinking..." bubble that gets replaced with the actual response.
- addMessage() appends styled message bubbles to the chat window and auto-scrolls to the latest message.
- Save index.html.
- Restart the application by clicking Rerun in the Run panel.
- Wait for Started InventoryChatbotApplication in the console.
Before you open the browser, think about what you expect to happen. If you log in as admin and ask "What are the total sales?", then log out and log in as customer with the same question, will the answers be the same or different?
- Open your browser and navigate to http://localhost:8080/index.html.
- The login form appears pre-filled with admin and admin123. Click Log In.
- Type What are the total sales? and press Enter.
Gemini responds with a system-wide answer including total revenue across all orders. The admin sees everything.
- Click Logout in the top right corner.
- Change the username to customer and the password to cust123.
- Click Log In.
- Ask the same question: What are the total sales?.
This time Gemini reports only Bob Customer's personal order data. The same question, completely different answers based on who is logged in.
- Try one more test. Type What is the weather? and press Enter.
Gemini responds that this is outside its scope. The system prompt's rules are working. Your chatbot only answers inventory questions.
Chat page not loading or login failing?
- If the browser shows a blank page or a 404, confirm that index.html is inside src/main/resources/static/ (not src/main/resources/templates/).
- If the login button does nothing, open your browser's Developer Tools (F12) and check the Console tab for JavaScript errors.
- If login succeeds but chat messages return errors, make sure you restarted the application after saving all files.
- Need more help?
Secret mission
Add Conversation Memory
Right now every message to the chatbot is completely independent. Ask "Which products are low in stock?" and then follow up with "Tell me more about the first one" and Gemini has no idea what "the first one" refers to. Add Spring AI's MessageChatMemoryAdvisor so the chatbot remembers previous messages within a session and supports natural follow-up conversations.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally with the Gemini API free tier, so there are no ongoing costs.
Resources you used:
- The running Spring Boot application in IntelliJ IDEA.
- The inventory_chatbot MySQL database with seeded users, products, and orders.
- Your Gemini API key from Google AI Studio.
- The project files on your local machine.
Keep everything running
No action needed. Choose this if you are still actively building or want to continue experimenting with the chatbot.
- Your Spring Boot application will keep running until you stop it or close IntelliJ IDEA.
- Your inventory_chatbot MySQL database will persist between restarts.
- Your Gemini API key stays active in Google AI Studio. It uses the free tier, so there are no charges.
Pause - I'll come back to this later
Stop the running application to free up memory and your terminal. Your files and database stay intact for when you return.
- In IntelliJ IDEA, click the red square Stop button in the toolbar to stop the Spring Boot application.
- Your project files and the inventory_chatbot database remain on your machine. To resume, reopen the project in IntelliJ IDEA and run it again.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to clean up completely.
- In IntelliJ IDEA, click the red square Stop button in the toolbar to stop the Spring Boot application.
- Drop the MySQL database by opening a MySQL terminal and running the command below:
mysql -u root -p -e "DROP DATABASE inventory_chatbot;"
- Delete your Gemini API key. Go to aistudio.google.com/apikey, find the key you created for this project, click the trash icon next to it, and confirm deletion.
- Delete the project files from your machine. Open a terminal and run the appropriate command for your operating system:
macOS
rm -rf ~/Desktop/inventory-chatbot
Replace ~/Desktop/inventory-chatbot with the actual path where you saved the project if you chose a different location.
Windows
rmdir /s /q %USERPROFILE%\Desktop\inventory-chatbot
Replace %USERPROFILE%\Desktop\inventory-chatbot with the actual path where you saved the project if you chose a different location.
Nice Work!
Nice Work!
You just built a full-stack AI chatbot that connects a MySQL database to Google Gemini through a secured Spring Boot backend. The same question now produces completely different AI answers depending on who is logged in.
What you learned:
- Built a Spring Boot backend that bridges MySQL and Gemini using the data bridge pattern, where your code queries the database and injects the results as context into the AI prompt.
- Implemented stateless JWT authentication with role-based access control that serves different inventory data to admins and customers from the same endpoint.
- Created a ChatService that builds role-specific system prompts with an out-of-scope guard, proving that the AI only answers questions grounded in your actual database records.
- Secret Mission: Added conversation memory with MessageChatMemoryAdvisor so the chatbot supports natural multi-turn conversations with follow-up references.
Ready to quiz yourself?