This document covers development setup and practices for the EnduranceTrio Tracker project. For an overview of the project, see the main README.md.
- Technology Stack
- API Key Management
- Database
- Installation
- Code & Naming Conventions
- Programmatic Version Management
- Java 21 - Latest LTS version for optimal performance and features
- Spring Boot 4.0.0 - Modern application framework with latest stable features
- Spring Data JPA - Robust data persistence and repository abstraction
- PostgreSQL 18 - An advanced Open Source Relational Database for data persistence
- H2 Database - In-memory database for testing
- Flyway - Database migration and version control
- Spring Security - API key authentication and security configuration
- SpringDoc OpenAPI - Automated Swagger/OpenAPI documentation generation
- Maven - Dependency management and build automation
The EnduranceTrio Tracker REST API uses secure API key authentication. All API keys are stored as bcrypt hashes in the database for enhanced security. This section explains how to generate secure API keys, create their bcrypt hashes, and store them in the database.
Generate cryptographically secure random API keys, with openssl, executing the following command:
openssl rand -base64 32The above command generates a 32-character secure API key. A 48-character secure API key (even more secure), can be generated executing the following command:
openssl rand -base64 48Using Python3 is the recommended method to generate bcrypt hashes. On Ubuntu/Debian, ensure the bcrypt library is installed with the following command:
sudo apt update && sudo apt install python3-bcryptThen, replace the placeholder in the below command as appropriate and execute it to generate the bcrypt hash from the previously generated API key.
python3 -c "import bcrypt; print(bcrypt.hashpw('{API_KEY}'.encode('utf-8'), bcrypt.gensalt(rounds=12)).decode('utf-8'))"Placeholder Definition
- {API_KEY} : The API key
The application supports automatic initialization of the first tracker account using environment variables. This is the recommended approach for initial setup.
The service responsible for initializing the first tracker account will be executed upon application
first startup. This service checks for the presence of environment variables FIRST_OWNER and
FIRST_HASH during application startup. If both variables are provided and valid, it creates
the initial tracker account in the database. If an account with the provided owner name already
exists in the database, its key hash will be overridden with the provided key hash.
# 1. Generate a secure API key
API_KEY=$(openssl rand -base64 32)
echo "Generated API Key: ${API_KEY}"
# 2. Set environment variables and start application
export FIRST_OWNER="system"
export FIRST_HASH=$(python3 -c "import bcrypt; print(bcrypt.hashpw('${API_KEY}'.encode('utf-8'), bcrypt.gensalt(rounds=12)).decode('utf-8'))")
# 3. Check if the environment variables are correct
echo "First Owner: ${FIRST_OWNER}"
echo "First Hash: ${FIRST_HASH}"
# IMPORTANT: Store the raw API key securely - you won't be able to retrieve it later!
# Only the bcrypt hash should be stored in the database.Access the database console, replace the placeholders in the below SQL command as appropriate and
execute it to insert the new account into the tracker_account table (see the
database section).
INSERT INTO tracker_account (owner, account_key, enabled, version, created_at)
VALUES ('{OWNER}', '{API_KEY_HASH}', TRUE, 0, CURRENT_TIMESTAMP);Placeholder Definition
- {OWNER} : The name of the owner/user of the API key
- {API_KEY_HASH} : The bcrypt hash of the API key (not the raw API key)
- Key Generation
- Use cryptographically secure random generators
- Minimum 32 characters length
- Base64 encoding for URL-safe characters
- Hash Storage
- Always use bcrypt with cost factor 12
- Never store raw API keys in the database
- Include account name and creation timestamp
- Operational Security
- Securely transmit the raw API key to the end user once
- Implement key rotation policies
- Monitor and audit API key usage
- Store the raw key securely during initial distribution
- When the application initial startup is completed, unset the environment variables
FIRST_OWNERandFIRST_HASH.
You can verify API keys work by testing with the provided endpoints using the
Authorization: Bearer api-key-here and ET-Owner: account-name-here headers as shown
in the API Endpoints section of the main README.md.
The application uses a PostgreSQL database and an H2 in-memory database, configured with PostgreSQL compatibility mode for testing purposes.
All database schema changes are managed with Flyway. Migration
scripts are located in the endurancetrio-data/src/main/resources/db/migration folder and are
automatically executed on application startup. Migrations support both H2 (test)
and PostgreSQL (development and production).
The file DATABASE.md documents the
development and management of the application's database.
Login into the PostgreSQL server, replace the placeholders in the commands below as appropriate, and execute them to create the database for the EnduranceTrio Tracker REST API:
sudo -u postgres psqlCREATE DATABASE {DATABASE_NAME} ENCODING = 'UTF8' LC_COLLATE = 'C.UTF-8' LC_CTYPE = 'C.UTF-8' TEMPLATE = template0;Placeholder Definition
- {DATABASE_NAME} : The name chosen for the new database;
Confirm that the database was created, then connect to it:
\l
\c {DATABASE_NAME}Create the schema for the EnduranceTrio Tracker REST API:
CREATE SCHEMA IF NOT EXISTS {SCHEMA_NAME};Placeholder Definition
- {SCHEMA_NAME} : The name chosen for the new schema;
Confirm the schema creation:
\dnCreate a user for the EnduranceTrio Tracker database/schema management:
CREATE USER {USERNAME} WITH PASSWORD '{PASSWORD}';Placeholder Definition
- {USERNAME} : The new account name in the PostgreSQL Server;
- {PASSWORD} : The password of the new account in the PostgreSQL Server.
Confirm that the user creation:
\duGrant the necessary permissions to the EnduranceTrio Tracker schema user:
GRANT CONNECT ON DATABASE {DATABASE_NAME} TO {USERNAME};
GRANT USAGE ON SCHEMA {SCHEMA_NAME} TO {USERNAME};
GRANT CREATE ON SCHEMA {SCHEMA_NAME} TO {USERNAME};
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA {SCHEMA_NAME} TO {USERNAME};
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA {SCHEMA_NAME} TO {USERNAME};
ALTER DEFAULT PRIVILEGES IN SCHEMA {SCHEMA_NAME} GRANT ALL PRIVILEGES ON TABLES TO {USERNAME};
ALTER DEFAULT PRIVILEGES IN SCHEMA {SCHEMA_NAME} GRANT ALL PRIVILEGES ON SEQUENCES TO {USERNAME};Placeholder Definition
- {DATABASE_NAME} : The name chosen for the new database;
- {SCHEMA_NAME} : The name chosen for the new schema;
- {USERNAME} : The account name in the PostgreSQL Server to whom the privileges will be assigned.
Confirm that the privileges were granted:
\lOnce all the commands were executes, exit the PostgreSQL server:
\qConnection refused: Ensure PostgreSQL is running:
sudo systemctl status postgresqlPermission denied: Verify user has proper grants:
SELECT * FROM information_schema.role_table_grants WHERE grantee = '{USERNAME}';- Java 21 or higher
- Apache Maven
- PostgreSQL
git clone git@github.com:EnduranceCode/endurancetrio-tracker.git
cd endurancetrio-trackerCreate the application-secrets.yaml configuration file from the provided
template, with the following
command:
cp endurancetrio-app/src/main/resources/template-secrets.yaml endurancetrio-app/src/main/resources/application-secrets.yamlNow, edit the application-secrets.yaml file:
- Set database credentials: replace
{USER}and{PASSWORD}with your desired values.
Security Notice
Never commit the application-secrets.yaml file, or any file containing credentials, to version control. Ensure that your
.gitignorerules prevent accidental commits of sensitive configuration.
From the repository root, run the following command to compile the application and install its dependencies:
mvn clean installThe application uses Spring Boot profiles for environment-specific configuration:
application-local.yaml– Active during local development.application-dev.yaml– For development environments.application-prod.yaml– For production environments.
You can manually activate a profile when running the application with spring-boot:run:
-Dspring-boot.run.profiles=localOr, for standard JAR execution:
-Dspring.profiles.active=devA helper script, launch-app.sh, is provided to streamline local development. It performs
a full Maven build and then starts the application using the packaged JAR with the local
profile enabled:
./launch-app.shThis project also includes an IntelliJ run configuration stored in the .run/ folder. After opening
the project in IntelliJ, you will find a TrackerApplication
entry in the run/debug configuration dropdown. Select it and run the application with
Shift + F10, or use Shift + F9 to run the application in debug mode.
The run configuration uses the local Spring profile (application-local.yaml), so you can start
developing immediately without additional setup.
This section establishes naming guidelines for controllers, services and repositories, based on clarity, maintainability, and semantic meaning.
- Interfaces define contracts; implementations should have meaningful names.
- Use suffixes that reflect the role or nature of the implementation (e.g.,
Main,Cached,Remote). - Avoid generic suffixes like
Implunless absolutely necessary. - Keep naming consistent across layers.
- Interface:
- Purpose: Hold OpenAPI annotations and define endpoint contracts.
- Naming:
DomainAPI(e.g.,UserAPI).
- Implementation:
- Annotate with
@EnduranceTrioRestController. - Naming:
DomainRestController(e.g.,UserRestController).
- Annotate with
public interface UserAPI {
/* OpenAPI annotations */
}
@EnduranceTrioRestController
public class UserRestController implements UserAPI {
/* endpoints */
}- Interface: Optional (usually not needed unless for documentation or testing).
- Naming:
DomainWeb(e.g.,UserWeb).
- Naming:
- Implementation:
- Annotate with
@Controller. - Naming:
EntityWebController(e.g.,UserWebController).
- Annotate with
@Controller
public class UserWebController {
/* Thymeleaf views */
}To facilitate navigation between the code and the live documentation, the order of the methods within the Controller implementation must strictly follow the display order of the Swagger/OpenAPI web page.
- Sequence: Methods should appear in the class in the same sequence they are rendered in the UI
(typically sorted by path and then by HTTP verb:
GET,POST,PUT,DELETE).- Example: A
GETrequest for/usersmust appear before aGETrequest for/users/{id}. - Example:
/users/activemust appear before/users/{id}.
- Example: A
- Grouping: Do not scatter related endpoints; keep the code structure linear to the documentation output.
- Interface:
- Naming:
DomainService(e.g.,UserService).
- Naming:
- Implementation:
- Annotate with
@Service. - Naming:
- If single implementation:
DomainServiceMain(e.g.,UserServiceMain). - If multiple implementations: Use descriptive suffixes (e.g.,
UserServiceCached,UserServiceRemote).
- If single implementation:
- Annotate with
public interface UserService {
/* business logic */
}
@Service
public class UserServiceMain implements UserService {
/* implementation */
}Why not
Impl?Implis semantically weak. Descriptive suffixes improve readability and convey purpose.
To promote maintainability, facilitate quick navigation through business logic as well as to impose semantic structure and align methods with the entity lifecycle, the methods within the Service implementation must be ordered based on their primary CRUD operation (Create, Read, Update, Delete).
-
Primary Grouping: Methods must first be grouped by the Domain Entity they primarily operate on.
-
Secondary Ordering (Within Group): Within each entity group, methods must be strictly ordered by the CRUD operation they perform:
- CREATE Operations (e.g.,
createUser,addAddress) - READ Operations (e.g.,
findById,findAllActive,existsByUsername) - UPDATE Operations (e.g.,
updateStatus,modifyPassword) - DELETE Operations (e.g.,
deleteById,removeAllInactive) - UTILITY/AD-HOC Operations (Any business logic that does not fit CRUD, placed at the end).
- CREATE Operations (e.g.,
-
Tertiary Ordering (Within CRUD Group): If multiple methods fall into the same CRUD category (e.g., multiple READ methods), they must be ordered alphabetically by their method name.
-
Example (User Entity):
addRole(Long userId, Role role)(CREATE)create(User user)(CREATE)findAllActive()(READ)findById(Long id)(READ)updateEmail(Long id, String newEmail)(UPDATE)deleteById(Long id)(DELETE)sendPasswordResetEmail(String email)(UTILITY)
Rationale: This order helps developers quickly locate methods based on what they do to the entity, providing a clear map of the entity's data lifecycle within the application.
Private methods, which support the public contract, must follow a consistent placement and ordering standard to improve internal code readability.
- Placement: All private methods must be placed after all public methods of the class.
- Ordering: Private methods must be ordered alphabetically by their method name. This provides the simplest and most predictable structure for implementation details.
- Prefer Spring Data JPA interfaces:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {}For Repository interfaces extending Spring Data JPA base classes (e.g., JpaRepository),
a comprehensive method ordering rule is not strictly necessary, as the core CRUD contract
is inherited.
- Inherited Methods: The inherited methods (
save,findById,findAll,deleteById, etc.) are implicitly defined first. - Custom Methods: Any custom query methods (defined by name or with
@Query) must be strictly ordered by the CRUD operation they perform. If multiple methods fall into the same CRUD category (e.g., multiple READ methods), they must be ordered alphabetically by their method name.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Custom query methods must be in alphabetical order:
boolean existsByUsername(String username);
List<User> findAllByStatus(UserStatus status);
Optional<User> findByEmail(String email);
}This project supports programmatic version updates across all Maven modules. It can be achieved replacing the label as appropriate in the below command and then executing it.
mvn versions:set -DnewVersion={VERSION_NUMBER}Placeholder Definition
- {VERSION_NUMBER} : The new Sematic Version number to be applied across all Maven modules
The changes applied with the above command can be reverted executing the following command:
mvn versions:revertOr commited with the following command:
mvn versions:commit