Skip to main content
Given the increasing integration of Machine Learning and AI/LLM models in all applications, it is crucial to explore the potential of a robust cloud application platform like Upsun for conducting these experiments. As a watch enthusiast, what would be better than a recommendation engine to find the right watch for you? Here is the app we are going to build from scratch here: Watch Recommendation App

Source

You can find the full example repository on Github: https://github.com/gmoigneu/upsun-embeddings-watches

Architecture

Let’s review the architecture we are going to create and deploy: Watch Recommendation Architecture Our application will be composed of 3 main components:
  • A static React frontend is used to present the UI to the user.
  • An API built with Laravel 11 will handle the search requests and populate our database.
  • A Postgres instance to store our watches records and vectors for the embeddings of our content.
For speed and cost efficiency reasons, we will leverage both OpenAI for creating embeddings and Groq for the LLM part. We will use llama3–8b-8192 as a model on Groq, but mixtral or any other would also work.

Bootstrapping our apps

You will need php8.x and composer installed locally for this to work. As we deploy our app on Upsun, you can grab the upsun cli by following the steps here. First, create a new folder for your project:
Let’s start by installing Laravel 11 with the API components:
You will need a few extra packages:
Let’s now create a new React app. We are using Vite here with the react-ts template but feel free to use any other variant. We will set up shacdn/ui for the UI to speed up building our interface.
Add the following code to the tsconfig.json file to resolve paths:
Add the following code to the vite.config.ts so your app can resolve paths without error
Now run the shacdn init with the following settings:
Add the needed components for our UI:
Our React app will use two extra packages:
Our two apps are now bootstrapped. Time to initialize our git repo:

Setting up our local development environment & infrastructure

While you could use Laravel Sail or any other Docker-based local setup to develop the project, I find relying on the Upsun tethering feature always way quicker. It maps the remote services (database, Redis, etc.) to local ports, so you only need to run the runtimes (PHP, JS, …) locally. To do that, we first need to go through the Upsun configuration. While you can use the upsun ify CLI command to generate an automatic configuration, here is the complete configuration explained. Create a new .upsun/config.yaml and paste the following:
Once done, we must inject some variables into our Laravel app to grab the proper credentials. Create the watches/.environment file with the following:
You can customize any other environment variable this way. Remember they will always be the same value as this is committed to our repo. Let’s add the configuration to our git repo:

Our first deploy

Let’s create an Upsun project for our application:
The Upsun bot will then provide an empty project for you and set the remote in your local git repository. Once done, you are ready to deploy:
The pipeline will build and deploy both apps. After a couple of minutes, the boilerplate apps will be available on: https://frontend.<branch>.<project>.<region>.platformsh.site https://app.<branch>.<project>.<region>.platformsh.site You should be directed to the default pages for both the React and Laravel applications. We can now connect to our remote database and cache services:
You can now edit your local watches/.env to use these services:
You will need your local PHP runtime to have the phpredis extension loaded. Please refer to the following documentation on the Laravel site. If you want to use test hostnames for your applications, edit /etc/hosts or your Windows host file with the following or any other names you like: 127.0.0.1 watches.test frontend.watches.test api.watches.test If so, update the .env file with the proper hostname:
We can now launch our development services. In two terminals, run:
Note the -- for the argument to be able to run. Your local applications are now available at http://frontend.watches.test:5137 and http://api.watches.test.

Getting some data about watches

As we will recommend a specific watch to our user, we must first have a database containing the potential results. Fortunately, Kaggle has a Luxury Watch Dataset (no longer available. Check https://www.kaggle.com/kashnitsky/datasets/ for other public data sets) available for download. Create an account if needed and download the CSV file. Kaggle dataset We will now move and rename the CSV file into our project:

Configuring our external services

For us to use OpenAI and Groq APIs, we need to inject our credentials. Create new API keys from their respective websites. In Laravel, we will add the respective arrays in the services configuration:
Once done, add the variables to your local .env file:
For them to work on Upsun, create two variables using the cli :

Let’s import our watches into the database

We will need a few things first. Create a Watch model. You can create it using php artisan make:model Watch --m or by adding the files manually:
Note in the model that we use our Vector custom type to store and cast the embedding column. Create the migration file for the model:
Our embedding columns will use the vector type from the pg_vector package. Migrate the database:
We now need to code a job that will import the watches and generate the embedding. Create the command and the job:
Our command is simple and only triggers the job:
Our Vectorize job is more complex. It will:
  • Parse the CSV and create the watches in the database
  • Combine all attributes of each watch into a text string that we will send to OpenAI to get an embedding
  • Store that embedding in our database as a vector
You can now run the job by triggering:
It should take a few seconds. Once done you can connect to your Postgres instance and check the results:
Perfect, we have our database of 508 watches and the embeddings created from their content.

Create the API controller to get the search request

Let’s create the API controller for our frontend to query the database. The controller will take care of:
  • Parsing and validating the query value sent through the POST request
  • Creating an embedding of the query
  • Query the database using nearestNeighbors to find the most relevant watch
  • Send a streamed request to the Grok Chat API to create a proper answer with the details of the watch and the reason why it is the best.
As the response is streamed from Grok, we automatically stream it to our client using an Event Stream. This will allow us to display the answer being generated in real-time instead of waiting for the full answer being processes.
As you can see, we are validating the POST request via the SearchWatchRequest validator. Remember to return true in the authorize() method, or you will get a 403 when POSTing to it.
In a production application, you must also validate that the user exists and is not abusing the system. You can leverage Laravel Sanctum for this.

Wrap up the Laravel configuration

Two tasks need to be done before our Laravel is fully operational. First, create a new route to handle the request:
Then, as we will query our API through a React frontend, we need to make CORS will be correctly set. Publish the CORS configuration file with:
Open cors.php and review the settings:

Let’s test our API!

You can now use curl or a tool like Postman to query the API and get your result. As you can see, the result is streamed in chunks by our Laravel API. Congratulations! API Test with Postman

Let’s deploy it!

Add the vectorize job in .upsun/config.yaml so it is done every time in the background after you deploy the app without impacting the app availability. You might want to trigger this manually if you have more data to avoid increasing your OpenAI bill too much.
The deploy log will output the URLs for both endpoint. You can now retest the API with Postman on the Upsun API hostname.

Time to build our frontend

Let’s switch to our frontend React app now.
As our UI will be really straightforward, we only need two basic shacdn/ui components:
The components are now installed in src/components/ui. We will use two different libraries to query our APIs:
  • axios would be used to query other potential endpoints of our Laravel API
  • @microsoft/fetch-event-source to stream the result from our API
Let’s install both:
We are now creating a shared axios instance for our project with the configuration and a method to define our API URL:
getApiURL will use our local development values if the hostname is watches.test. If not, it will replace the frontend string with api on any other hostname. This will allow us to make it functional in any preview environment on Upsun as well as production. You can always change the logic there to map your own deployment needs.

The main App.tsx

It’s time to build that frontend application layout and logic:
How it works:
  • The JSX is rendered on load. We use a <Markdown/> field to render the answer.
  • The user writes a prompt in the main <TextArea/> that is store in a variable prompt using useState.
  • When clicking the <Button/> , the script triggers submitForm() which creates a newStreamResponse
  • newStreamResponse starts by setting the state variable streaming as true so we can use it to freeze our UI while the request loads and reset the answer as well as creating a local variable named messageContent to store the stream content coming back from the API.
  • It then uses fetchEventSource to query our Laravel API. The openWhenHidden option specifies if the stream should be listened to when the tab or window is not in focus. We pass our prompt as the body of this POST request.
  • The onmessage method defines how we treat incoming data from the Event stream. Here we concatenate the new chunk to our messageContent and then update the state variable answer to show the result. In case your content is too long or the chunks are coming in too fast, you might want to delay the update to avoid too many renderings.
  • When the stream ends, we reset the streaming variable to false.

Final deploy

Once these files added, let’s deploy:

The result

You can now use the frontend URL that Upsun gave you in the deploy log to access your app. Let’s query it! Final Application Demo Everything works as expected! You can see in my recording above, that I am using a custom hostname. In your Upsun console, click the Go Live button and follow the instructions to configure your DNS and setup the domain! Setup domain

Conclusion

While this example is stripped down to the core, it is interesting to see how you can leverage embeddings for search, classification or any other Machine Learning process. With all the tooling now available, it becomes super accessible to everyone to query models and run these applications on a Cloud Applications Platform like Upsun.com.
Last modified on April 17, 2026