---
title: "Introducing Spring Native for JHipster: Serverless Full-Stack Made Easy"
description: "The newly released JHipster Native blueprint allows you to generate Spring Boot projects with Spring Native support."
authors:
  - name: "Matt Raible"
    url: "https://auth0.com/blog/authors/matt-raible/"
date: "Apr 25, 2022"
category: "Developers,Tutorial,JHipster"
tags: ["spring", "native", "jhipster"]
url: "https://auth0.com/blog/spring-native-jhipster/"
---

# Introducing Spring Native for JHipster: Serverless Full-Stack Made Easy

<div class="alert alert-info alert-icon">
  <i class="icon-budicon-487"></i>
  <strong>Important Information</strong><br> 
  Originally published at <a href="https://developer.okta.com/blog/2022/03/03/spring-native-jhipster">developer.okta.com</a>.<br> 
</div>

Over the years, I’ve developed a lot of Java applications. I started writing Java code in the late 90s and spent several years doing Java before I tried another server-side language. I was impressed when I first tried building apps in Ruby on Rails, Python, and Node.js - they all started super-fast!

Starting fast is cool, but we in the Java community have often asked, does it perform over time? The Java Virtual Machine is famous for performance and optimization over time.

I scoffed at serverless when it first came out. Mostly because I was a Java developer, and my apps didn’t start in milliseconds. They also used a whole lotta memory, and there was no hope in sight.

Then, along came [GraalVM](https://www.graalvm.org/). It’s gained support from many Java frameworks in the last few years and has made their apps start in milliseconds!

Today, I’m proud to announce that this same capability is now available for your JHipster 7+ apps!

**Prerequisites**:

* [Node 14](https://nodejs.org/)+
* [Java 17 with GraalVM](https://sdkman.io/)+
* [Docker Desktop](https://docs.docker.com/desktop/#download-and-install)

<br />

## Why Should You Care about Serverless?

From [IBM’s What is Serverless computing?](https://www.ibm.com/cloud/learn/serverless)

> Serverless is a cloud execution model that enables a simpler, more cost-effective way to build and operate cloud-native applications.

That’s it!

For companies with high traffic and large cloud bills, serverless makes sense. They can save millions of dollars a month.

## GraalVM Allows Serverless Java for Everyone!

I’ve been playing with GraalVM and the Java frameworks that support it for around 18 months. After doing a lot of research, I blogged about [building native Java apps with Micronaut, Quarkus, and Spring Boot](https://developer.okta.com/blog/2021/06/18/native-java-framework-comparison).

In September 2021, I did a talk with Josh Long about Spring Native with JHipster at the San Francisco JUG.

<NonAmpContent>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Let the games begin with <a href="https://twitter.com/starbuxman?ref_src=twsrc%5Etfw">@starbuxman</a>! <a href="https://twitter.com/hashtag/Java?src=hash&amp;ref_src=twsrc%5Etfw">#Java</a> <a href="https://twitter.com/hashtag/SpringNative?src=hash&amp;ref_src=twsrc%5Etfw">#SpringNative</a> <a href="https://twitter.com/hashtag/JHipster?src=hash&amp;ref_src=twsrc%5Etfw">#JHipster</a> <a href="https://t.co/0KSRTd89e5">pic.twitter.com/0KSRTd89e5</a></p>&mdash; Matt Raible (@mraible) <a href="https://twitter.com/mraible/status/1442698739563446276?ref_src=twsrc%5Etfw">September 28, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</NonAmpContent>

I flew out on a Monday; we got everything working by Wednesday afternoon and then spoke about our experience on Wednesday night. I wrote about it in [JHipster Works with Spring Native!](https://www.linkedin.com/pulse/jhipster-works-spring-native-matt-raible/) on LinkedIn and created a [JHipster issue](https://github.com/jhipster/generator-jhipster/issues/16498) to automate our learnings.

## What is Spring Native?

Spring Native provides an API to configure Spring Boot and GraalVM, so classes that are not easily discoverable become recognizable. For example, those that are instantiated using reflection. It’s a really slick extension to Spring Boot and will likely disappear with Spring Boot 3 because it’ll be native by default.

As a result of our successful [presentation at the SF JUG](https://youtu.be/F9oydL_MndA), Josh and I were invited to speak at the Garden State JUG in December. We laid low for a few months and rekindled our research in December. I wrote about it in [JHipster works with Spring Native, Part 2!](https://www.linkedin.com/pulse/jhipster-works-spring-native-part-2-matt-raible/)

## 🔥 Announcing the JHipster Native Blueprint!

In late January 2022, we (the JHipster team) upgraded JHipster to use Spring Boot 2.6 and released version [7.6.0](https://www.jhipster.tech/2022/01/23/jhipster-release-7.6.0.html).

In early February, I updated [the Spring Native with JHipster examples](https://github.com/mraible/spring-native-examples) that Josh and I’d been using for research. I was ready to start automating the Spring Native integration using a JHipster module. When [I asked the JHipster team](https://github.com/jhipster/generator-jhipster/issues/16498#issuecomment-1030263905) about the best way to implement it, [Marcelo Shima](https://github.com/mshima) volunteered to create the initial blueprint.

This was on a Friday afternoon (Mountain Time or MT). By the next morning, Marcelo had the MVP finished and provided steps to reproduce our examples. 😳

I was amazed! I remember telling (my partner) Trish that he’d done a week’s worth of (my estimated) work in a matter of hours.

Today, I’m proud to announce the [JHipster Native blueprint](https://github.com/jhipster/generator-jhipster-native) is available! Here’s how to use it:

```
npm install -g generator-jhipster-native
jhipster-native
```

This will generate a JHipster app and integrate GraalVM automatically. You can build and run a native image with the following commands:

```
./mvnw package -Pnative,prod -DskipTests
npm run ci:e2e:prepare # start docker dependencies
./target/native-executable
```

**NOTE**: Gradle is not currently an option. Follow [generator-jhipster-native#24](https://github.com/jhipster/generator-jhipster-native/issues/24) to see when it's supported.

## Go Native with Spring Native and GraalVM

To see Spring Native + JHipster in action, let’s look at a [previous JHipster app I created for the Auth0 blog](https://auth0.com/blog/full-stack-java-with-react-spring-boot-and-jhipster/). First, clone the example:

```
git clone https://github.com/oktadev/auth0-full-stack-java-example.git jhipster-native
cd jhipster-native
```

Want results right away? Clone the `spring-native` branch with the changes below already made:

```
git clone -b spring-native https://github.com/oktadev/auth0-full-stack-java-example.git jhipster-native
```

Then, skip to the [Configure your OpenID Connect identity provider](#Configure-Your-OpenID-Connect-Identity-Provider) section to continue.


Install JHipster 7.8.1 and the JHipster Native blueprint:

```
npm i -g generator-jhipster@7.8.1
npm i -g generator-jhipster-native@1.1.2
```

Then, remove all the existing project files and regenerate them. The `jhipster-native` command includes parameters to disable caching because it’s not supported by Spring Native yet.

```
rm -rf *
jhipster-native --with-entities --cache-provider no --no-enable-hibernate-cache
# When prompted to overwrite .gitignore, type "a" to overwrite all files
```

If you open the project in IntelliJ IDEA, you can use the **Commit Tools Window** (⌘+0 on macOS or Ctrl+0 on Linux/Windows) to view files that changed.

![JHipster](https://images.ctfassets.net/23aumh6u8s0i/3r7MSfWD4m4vVxbEjT9aQA/65413255ac14f395ef963381eb843a7b/01_commit-tools-window.jpeg)

Next, run the following `git checkout` commands to restore the files that were modified in the original example.

```
git checkout .gitignore
git checkout README.md
git checkout demo.adoc
git checkout flickr2.jdl
git checkout screenshots
git checkout src/main/webapp/app/entities/photo/photo.tsx
git checkout src/main/webapp/app/entities/photo/photo-update.tsx
git checkout src/main/java/com/auth0/flickr2/config/SecurityConfiguration.java
git checkout src/main/resources/config/application-heroku.yml
git checkout src/main/resources/config/bootstrap-heroku.yml
git checkout Procfile
git checkout system.properties
```

If you’d rather not use the command line, you can right-click on each file and select **Rollback**.

![JHipster](https://images.ctfassets.net/23aumh6u8s0i/1I2Le2GG01CdNiIbvTP3u7/287ac8531e2b7f24b982d1c5b010d6c0/04_rollback-commit.jpeg)

If you ran the `git checkout` commands, there are several changes I made in the first tutorial that’ll need to be re-applied:

1. In `src/main/resources/config/application-dev.yml`, remove the `faker` profile for Liquibase.
2. In `pom.xml`, re-add Drew Noake’s `metadata-extractor` library:

```
    <groupId>com.drewnoakes</groupId>
    <artifactId>metadata-extractor</artifactId>
    <version>2.16.0</version>
```

3. Next, modify the `createPhoto()` method in `src/main/java/com/auth0/flickr2/web/rest/PhotoResource.java` to set the metadata when an image is uploaded.

```js
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.jpeg.JpegDirectory;

import javax.xml.bind.DatatypeConverter;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import java.time.Instant;
import java.util.Date;

public class PhotoResource {
    ...

    public ResponseEntity<Photo> createPhoto(@Valid @RequestBody Photo photo) throws URISyntaxException {
        log.debug("REST request to save Photo : {}", photo);
        if (photo.getId() != null) { ... }

        try {
            photo = setMetadata(photo);
        } catch (ImageProcessingException | IOException | MetadataException ipe) {
            log.error(ipe.getMessage());
        }

        Photo result = photoRepository.save(photo);
        ...
    }

    private Photo setMetadata(Photo photo) throws ImageProcessingException, IOException, MetadataException {
        String str = DatatypeConverter.printBase64Binary(photo.getImage());
        byte[] data2 = DatatypeConverter.parseBase64Binary(str);
        InputStream inputStream = new ByteArrayInputStream(data2);
        BufferedInputStream bis = new BufferedInputStream(inputStream);
        Metadata metadata = ImageMetadataReader.readMetadata(bis);
        ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);

        if (directory != null) {
            Date date = directory.getDateDigitized();
            if (date != null) {
                photo.setTaken(date.toInstant());
            }
        }

        if (photo.getTaken() == null) {
            log.debug("Photo EXIF date digitized not available, setting taken on date to now...");
            photo.setTaken(Instant.now());
        }

        photo.setUploaded(Instant.now());

        JpegDirectory jpgDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
        if (jpgDirectory != null) {
            photo.setHeight(jpgDirectory.getImageHeight());
            photo.setWidth(jpgDirectory.getImageWidth());
        }

        return photo;
    }
    ...
```

4. Install the React libraries needed:

```
npm i react-photo-album react-images
```

<div class="alert alert-info alert-icon">
  <i class="icon-budicon-487"></i>
  <strong>IMPORTANT</strong><br> 
In the previous tutorial, I used `react-photo-gallery`. I switched to `react-photo-album` because <a href="https://github.com/neptunian/react-photo-gallery/issues/205#issuecomment-1086995379">it supports React 17</a>. Because of this, you'll also need to update `src/main/webapp/app/entities/photo/photo.tsx`.

 1. Change `import Gallery from 'react-photo-gallery'` to `import PhotoAlbum from 'react-photo-album'`.
 2. Change `<Gallery photos={photoSet} onClick={openLightbox} />` to `<PhotoAlbum photos={photoSet} layout="rows" onClick={openLightbox} />`.
 <br> 
</div>

5. In `src/test/javascript/cypress/integration/entity/photo.spec.ts`, remove the code that sets the calculated data in the `should create an instance of Photo` test:

```
cy.get(`[data-cy="height"]`).type('99459').should('have.value', '99459');
cy.get(`[data-cy="width"]`).type('61514').should('have.value', '61514');
cy.get(`[data-cy="taken"]`).type('2021-10-11T16:46').should('have.value', '2021-10-11T16:46');
cy.get(`[data-cy="uploaded"]`).type('2021-10-11T15:23').should('have.value', '2021-10-11T15:23');
```

Then, you’ll need to add type hints for Drew Noake’s EXIF processing library in `src/main/java/com/auth0/flickr2/Flickr2App.java`:

```js
@org.springframework.nativex.hint.TypeHint(
    types = {
        ...
        com.drew.metadata.exif.ExifIFD0Directory.class,
        com.drew.metadata.exif.ExifSubIFDDirectory.class,
        com.drew.metadata.exif.ExifThumbnailDirectory.class,
        com.drew.metadata.exif.makernotes.AppleMakernoteDirectory.class,
        com.drew.metadata.exif.GpsDirectory.class,
})
@org.springframework.nativex.hint.NativeHint(options = "-H:+AddAllCharsets")
```

The `@NativeHint(options = “-H:+AddAllCharsets”)` solves the following exception that happens when you upload a photo:

```
Caused by: java.nio.charset.UnsupportedCharsetException: Cp1252
    at java.nio.charset.Charset.forName(Charset.java:528) ~[native-executable:na]
    at com.drew.lang.Charsets.<clinit>(Charsets.java:40) ~[na:na]
```

Once you’ve made all the changes (or cloned the `spring-native` branch), you can build your hip native binary.

### Build a Native JHipster App

You will need a JDK with GraalVM and its `native-image` compiler. Using SDKMAN, run the following command and set it as the default:

```
sdk install java 22.0.0.2.r17-grl
```

Then, use Maven to build the project. Skip tests since there’s no support for Mockito at this time.

```
./mvnw package -Pnative,prod -DskipTests
```

This process will take a few minutes to complete.

## Configure Your OpenID Connect Identity Provider

When you generate a JHipster app with OAuth 2.0 / OIDC for authentication, it defaults to using Keycloak. It creates a `src/main/docker/keycloak.yml` file for Docker Compose, as well as a `src/main/docker/realm-config` directory with files to auto-create users and OIDC clients.

If you want to use Keycloak for your running app, start it with the following command:

```
docker-compose -f src/main/docker/keycloak.yml up -d
```

If you’d rather use Okta or Auth0, that’s possible too!

#### Use Okta as your identity provider

Before you begin, you’ll need a free Okta developer account. Install the [Okta CLI](https://cli.okta.com/) and run `okta register `to sign up for a new account. If you already have an account, run `okta login`. Then, run `okta apps create jhipster`. Select the default app name, or change it as you see fit. Accept the default Redirect URI values provided for you.

<details>
  <summary>What does the Okta CLI do?</summary>
The Okta CLI streamlines configuring a JHipster app and does several things for you:

1. Creates an OIDC app with the correct redirect URIs:
    * login: `http://localhost:8080/login/oauth2/code/oidc` and `http://localhost:8761/login/oauth2/code/oidc`
    * logout: `http://localhost:8080` and `http://localhost:8761`
2. Creates `ROLE_ADMIN` and `ROLE_USER` groups that JHipster expects
3. Adds your current user to the `ROLE_ADMIN` and `ROLE_USER` groups
4. Creates a `groups` claim in your default authorization server and adds the user’s groups to it

**NOTE**: The `http://localhost:8761*` redirect URIs are for the JHipster Registry, which is often used when creating microservices with JHipster. The Okta CLI adds these by default.
</details>


You will see output like the following when it’s finished:

```
Okta application configuration has been written to: /path/to/app/.okta.env
```

Run `cat .okta.env` (or `type .okta.env` on Windows) to see the issuer and credentials for your app. It will look like this (except the placeholder values will be populated):

```
export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI="https://{yourOktaDomain}/oauth2/default"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID="{clientId}"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET="{clientSecret}"
```

**NOTE**: You can also use the Okta Admin Console to create your app. See [Create a JHipster App on Okta](https://www.jhipster.tech/security/#okta) for more information.

You’ll need to source the `.okta.env` file the Okta CLI creates to override the default Spring Security settings.

```
source .okta.env
```

If you’re on Windows, you can modify this file to use `set` instead of `export` and rename it to `okta.bat`. Then, run it with `okta.bat` from the command line.

Modify your existing `.gitignore` file to have `*.env` so you don’t accidentally check in your secrets!

_Skip to [Run your native JHipster app](https://developer.okta.com/blog/2022/03/03/spring-native-jhipster#run-your-native-jhipster-app) if you’ve configured your app for Okta and just want to see it running._

#### Use Auth0 as your identity provider

To switch from Keycloak to Auth0, override the Spring Security OAuth properties. You don’t even need to write any code!

To see how it works, create a `.auth0.env` file in the root of your project, and fill it with the code below to override the default OIDC settings:

```
export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://<your-auth0-domain>/
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=<your-client-id>
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=<your-client-secret>
export JHIPSTER_SECURITY_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/
```

You’ll need to create a new web application in Auth0 and fill in the `&lt;…​>` placeholders before this works.

##### Create an OpenID Connect app on Auth0

Log in to your Auth0 account (or [sign up](https://auth0.com/signup) if you don’t have an account). You should have a unique domain like `dev-xxx.eu.auth0.com`.

Press the **Create Application** button in the [Applications section](https://manage.auth0.com/#/applications). Use a name like `JHipster Native!`, select `Regular Web Applications`, and click **Create**.

Switch to the **Settings** tab and configure your application settings:

* Allowed Callback URLs: `http://localhost:8080/login/oauth2/code/oidc`
* Allowed Logout URLs: `http://localhost:8080/`

Scroll to the bottom and click **Save Changes**.

Copy your Auth0 domain, client ID, and client secret into the `.auth0.env` file you created earlier. Then, run `source .auth0.env`.

In the [roles](https://manage.auth0.com/#/roles) section, create new roles named `ROLE_ADMIN` and `ROLE_USER`.

Create a new user account in the [users](https://manage.auth0.com/#/users) section. Click on the **Role** tab to assign the roles you just created to the new account.

_Make sure your new user’s email is verified before attempting to log in!_

Next, head to **Auth Pipeline** > **Rules** > **Create**. Select the `Empty rule` template. Provide a meaningful name like `Group claims` and replace the Script content with the following.

```js
function(user, context, callback) {
  user.preferred_username = user.email;
  const roles = (context.authorization || {}).roles;
  function prepareCustomClaimKey(claim) {
    return `https://www.jhipster.tech/${claim}`;
  }
  const rolesClaim = prepareCustomClaimKey('roles');
  if (context.idToken) {
    context.idToken[rolesClaim] = roles;
  }
  if (context.accessToken) {
    context.accessToken[rolesClaim] = roles;
  }
  callback(null, user, context);
}
```

This code is adding the user’s roles to a custom claim (prefixed with `https://www.jhipster.tech/roles`). Click **Save changes** to continue.

Want to have all these steps automated for you? Add a 👍 to [issue #351](https://github.com/auth0/auth0-cli/issues/351) in the Auth0 CLI project.


## Run Your Native JHipster App

After you’ve built your app, it will be available in `target/native-executable`. Start Keycloak or source your Okta/Auth0 settings. Then, run the following commands:

```
npm run ci:e2e:prepare # start docker dependencies
./target/native-executable
```

It should start in under a second!

![JHipster](https://images.ctfassets.net/23aumh6u8s0i/1ku2f3ONDrVZiu0PP95sdA/7a2c169fe9aa707bce1ac3858c0668f5/03_native-start.png)

## What Does the JHipster Native Blueprint Do?

The JHipster Native blueprint integrates Spring Native into a JHipster project based on [findings from the research by Josh Long and me](https://github.com/mraible/spring-native-examples#readme). I documented our findings in September and December 2021.

* Sep 30, 2021: [JHipster Works with Spring Native!](https://www.linkedin.com/pulse/jhipster-works-spring-native-matt-raible/)
* Dec 14, 2021: [JHipster works with Spring Native, Part 2!](https://www.linkedin.com/pulse/jhipster-works-spring-native-part-2-matt-raible/)

The unexpected thing is one of the hardest problems we had to solve was with JPA and relationships. In JVM mode, everything worked fine. When running in native mode, [there was an exception](https://github.com/jhipster/generator-jhipster/issues/17794). The solution took days to figure out but was quite simple to fix: I just had to add a type hint for `java.util.HashSet.class`. 🤯

During this experience, I was surprised to find that Spring Native [doesn’t support caching yet](https://github.com/spring-projects-experimental/spring-native/issues/465). I believe this support will be added by the community soon. In the meantime, if you’re looking to start/stop your infra as fast as possible, you probably don’t care about caching. Caching is made for long-lived, JVM-strong, JVM-loving apps.

## What’s the Performance Like?

The native binary starts in just over 500ms (577ms) on my 2019 MacBook Pro with a 2.4 GHz 8-Core Intel Core i9 processor and 64 GB of RAM.

If I start it in JVM mode with Maven, it takes a little over four seconds.

![JHipster](https://images.ctfassets.net/23aumh6u8s0i/7iu99wCMrvyl4KbjXJo4wH/b097bdeba7786709cda16057cd73f064/02_jvm-start.png)

As far as build time goes, Spring Native says:
```
Finished generating ‘native-executable’ in 3m 15s.
```

If I build a Docker image with the native binary:
```
mvn spring-boot:build-image -Pprod
```

It takes a while the first time:
```
Total time:  07:24 min
```

And it’s slightly faster the second time:
```
Total time:  06:43 min
```

The amount of memory used after starting: `178 MB`. <br>
The amount of memory used after running `npm run e2e`: `211 MB`. <br>
In the interest of full disclosure, here’s the command I used to measure the amount of memory used:
```
ps -o pid,rss,command | grep --color native | awk '{$2=int($2/1024)" MB";}{ print;}'
```

What about the M1 Max? That’s [not an option yet](https://twitter.com/mraible/status/1494417051137585152), but [could be in the next release of GraalVM](https://github.com/oracle/graal/issues/2666#issuecomment-1057819284).

## Learn More about GraalVM and Spring Native

I hope you’ve enjoyed learning how to make JHipster work with Spring Native and GraalVM. It’s still a work-in-progress. Startup time isn’t the end-all-be-all metric, but it is important in a serverless environment.

You can find the source code for this example on GitHub in the [@oktadev/auth0-full-stack-java-example repository](https://github.com/oktadev/auth0-full-stack-java-example/tree/spring-native). The source for the JHipster Native blueprint is in the [@jhipster/generator-jhipster-native repository](https://github.com/jhipster/generator-jhipster-native).

JHipster also has Micronaut and Quarkus blueprints. However, their native support is currently a work-in-progress. I hope to help improve them in the next few months.

* [JHipster Quarkus can’t build native image](https://github.com/jhipster/generator-jhipster-quarkus/issues/222)
* [JHipster Micronaut can’t build native image](https://github.com/jhipster/generator-jhipster-micronaut/issues/115)

If you liked this post, there’s a good chance you’ll like similar ones:

* [Watch GraalVM turn your Java into binaries](https://developer.okta.com/blog/2019/11/27/graalvm-java-binaries)
* [Spring Native in Action with the Okta Spring Boot Starter](https://developer.okta.com/blog/2021/09/16/spring-native-okta-starter)
* [Build Native Java Apps with Micronaut, Quarkus, and Spring Boot](https://developer.okta.com/blog/2021/06/18/native-java-framework-comparison)
* [Build REST APIs and Native Java Apps with Helidon](https://developer.okta.com/blog/2022/01/06/native-java-helidon)

If you have questions, please ask them in the comments below! If you’re into social media, follow us: [@oktadev on Twitter](https://twitter.com/oktadev), [Okta for Developers on LinkedIn](https://www.linkedin.com/company/oktadev), and [OktaDev](https://www.facebook.com/oktadevelopers) on Facebook. If you like learning via video, subscribe to [our YouTube channel](https://youtube.com/oktadev).