This guide provides a step-by-step walkthrough for building a Node.js application using the Fastify framework to send SMS messages via the Vonage Messages API. We will cover everything from project setup and Vonage configuration to implementing the core API endpoint, adding security measures, and handling potential issues.
By the end of this tutorial, you will have a functional Fastify API endpoint capable of accepting a phone number and message payload, and using Vonage to deliver the SMS. This solves the common need for applications to programmatically send transactional or notification SMS messages.
Prerequisites:
- Node.js (LTS version recommended) and npm installed.
- A Vonage API account.
- Basic familiarity with Node.js, asynchronous programming, and REST APIs.
- A tool for making HTTP requests (like cURL or Postman).
Technology Stack:
- Node.js: JavaScript runtime environment.
- Fastify: A high-performance, low-overhead web framework for Node.js. Chosen for its speed, extensibility, and developer experience.
- Vonage Messages API: A powerful API for sending messages across various channels, including SMS. We use this for reliable SMS delivery.
@vonage/server-sdk
: The official Vonage Node.js SDK for interacting with Vonage APIs.dotenv
: Module required byfastify-env
to load environment variables from a.env
file during development.fastify-env
: Fastify plugin for loading and validating environment variables.fastify-rate-limit
: Fastify plugin for basic rate limiting.@sinclair/typebox
: Used for defining validation schemas for Fastify routes and environment variables.
System Architecture:
The architecture is straightforward:
- A client (e.g., Postman, cURL, another application) sends a POST request to our Fastify API endpoint (
/api/v1/send-sms
). - The Fastify application receives the request, validates the input (phone number, message).
- The application uses the Vonage Node.js SDK, configured with your Vonage Application ID and Private Key, to call the Vonage Messages API.
- Vonage handles the delivery of the SMS message to the recipient's phone.
- The Vonage API returns a response (e.g., message ID) to our Fastify application.
- The Fastify application sends a success or error response back to the client.
(Note: A Mermaid diagram illustrating this flow was present in the original text but has been removed for standard Markdown compatibility.)
Final Outcome:
A simple but robust Node.js Fastify API server with a single endpoint (POST /api/v1/send-sms
) that securely sends SMS messages using Vonage.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir fastify-vonage-sms cd fastify-vonage-sms
-
Initialize Node.js Project: This command creates a
package.json
file.npm init -y
-
Install Dependencies: We need Fastify, the Vonage SDK,
dotenv
(forfastify-env
),fastify-env
itself,fastify-rate-limit
, and@sinclair/typebox
for schema validation.npm install fastify @vonage/server-sdk dotenv fastify-env fastify-rate-limit @sinclair/typebox
Install
pino-pretty
as a development dependency for readable logs:npm install --save-dev pino-pretty
-
Set up Project Structure: Create the following basic structure:
fastify-vonage-sms/ ├── node_modules/ ├── src/ │ ├── plugins/ │ │ └── env.js # Environment variable configuration │ ├── routes/ │ │ └── sms.js # SMS sending route │ ├── services/ │ │ └── vonage.js # Vonage SDK interaction logic │ └── app.js # Fastify application setup ├── .env.example # Example environment variables ├── .env # Local environment variables (DO NOT COMMIT) ├── .gitignore # Git ignore file ├── package.json └── server.js # Server entry point
src/
: Contains the core application logic.plugins/
: For Fastify plugins like environment variable handling.routes/
: Defines API endpoints.services/
: Encapsulates external service interactions (like Vonage).app.js
: Configures and exports the Fastify instance.server.js
: Imports the app and starts the server..env
/.env.example
: For managing sensitive credentials..gitignore
: Prevents committing sensitive files and unnecessary directories.
-
Create
.gitignore
: Create a.gitignore
file in the root directory with the following content to avoid committing sensitive information and build artifacts:# .gitignore node_modules .env npm-debug.log* yarn-debug.log* yarn-error.log*
-
Create
.env.example
: Create an.env.example
file in the root directory. This serves as a template for required environment variables.# .env.example # Server Configuration PORT=3000 HOST=0.0.0.0 # Vonage API Credentials & Configuration # Found in your Vonage Dashboard -> API Settings VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Application Credentials (for Messages API) # Create an application in Vonage Dashboard -> Applications VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root OR the key content itself # Vonage Number or Sender ID # Must be a Vonage virtual number associated with your application # or an approved Alphanumeric Sender ID VONAGE_SMS_FROM=YOUR_VONAGE_NUMBER_OR_SENDER_ID
-
Create
.env
File: Duplicate.env.example
to create your local.env
file. You will populate this with your actual Vonage credentials later.cp .env.example .env
Important: Never commit your
.env
file to version control.
2. Integrating with Vonage (Third-Party Service)
Before writing code, we need to configure Vonage and obtain the necessary credentials. The Messages API often uses Application ID and Private Key authentication for enhanced security.
-
Sign Up/Log In: Ensure you have a Vonage API account. New accounts usually start with free credit.
-
Set Messages API as Default (Recommended):
- Navigate to your Vonage API Dashboard.
- Go to Settings.
- Under API Keys -> SMS Settings, ensure ""Default SMS Setting"" is set to Messages API. This ensures consistency if you use other Vonage features.
- Click Save changes.
-
Get API Key and Secret:
- These are typically found at the top of your main Vonage API Dashboard page.
- Copy the API Key and API Secret.
- Paste them into your
.env
file for theVONAGE_API_KEY
andVONAGE_API_SECRET
variables. - Purpose: While the Messages API primarily uses the Application ID and Private Key for authentication in this setup, the
@vonage/server-sdk
might still require the API Key/Secret for its initialization process or potentially for other internal API calls it might make. It's best practice to include them if available.
-
Create a Vonage Application: The Messages API requires a Vonage Application to manage authentication and settings like webhooks (though we aren't using webhooks for sending SMS in this guide).
- Navigate to Applications in the Vonage Dashboard.
- Click Create a new application.
- Give it a descriptive name (e.g., ""Fastify SMS Sender"").
- Under Capabilities, enable Messages.
- You will see fields for Inbound URL and Status URL. Since we are only sending SMS in this guide, you can enter placeholder URLs (e.g.,
https://example.com/inbound
,https://example.com/status
). If you later want to receive SMS or delivery receipts, you'll need to update these with real endpoints exposed via tools like ngrok for local development. - Click Generate public and private key. This will automatically download a
private.key
file. Save this file securely. A good practice is to place it in the root of your project directory (fastify-vonage-sms/private.key
). Do not commit this file to version control. - Click Generate new application.
- You will be taken to the application's page. Copy the Application ID.
- Paste the Application ID into your
.env
file forVONAGE_APPLICATION_ID
. - Update
VONAGE_PRIVATE_KEY_PATH
in your.env
file to the correct path where you saved the key (e.g.,./private.key
if it's in the root). Alternatively, you can store the key content directly in an environment variable later (see Section 8).
-
Link a Vonage Number: You need a Vonage virtual number to send SMS messages from.
- On the application page you just created, scroll down to Link virtual numbers.
- Click Link next to an available Vonage number you own. If you don't have one, you'll need to buy one via the Numbers -> Buy numbers section of the dashboard.
- Copy the linked Vonage number (in E.164 format, e.g.,
14155550100
). - Paste this number into your
.env
file forVONAGE_SMS_FROM
. Alternatively, if approved for your account, you can use an Alphanumeric Sender ID (e.g., ""MyApp"").
-
Whitelist Test Numbers (Trial Accounts): If you are using a trial Vonage account, you can typically only send SMS messages to phone numbers that you have verified and added to your test list.
- Navigate to Numbers -> Test numbers.
- Add the phone number(s) you intend to send test messages to. You will need to verify ownership via a code sent to that number.
Now your .env
file should contain all the necessary credentials.
3. Implementing Core Functionality & API Layer
We'll now set up the Fastify server, configure environment variables, create the Vonage service, and define the API route.
-
Configure Environment Variables Plugin (
src/plugins/env.js
): This plugin usesfastify-env
to load and validate the environment variables defined in our.env
file.// src/plugins/env.js import fp from 'fastify-plugin'; import fastifyEnv from 'fastify-env'; import { Type } from '@sinclair/typebox'; // For schema validation const schema = Type.Object({ PORT: Type.Number({ default: 3000 }), HOST: Type.String({ default: '0.0.0.0' }), VONAGE_API_KEY: Type.String(), VONAGE_API_SECRET: Type.String(), VONAGE_APPLICATION_ID: Type.String(), VONAGE_PRIVATE_KEY_PATH: Type.String(), // Can be path or key content VONAGE_SMS_FROM: Type.String(), }); async function configLoader(fastify, options) { await fastify.register(fastifyEnv, { dotenv: true, // Load .env file using dotenv schema: schema, confKey: 'config', // Access variables via fastify.config }); } export default fp(configLoader);
- We define a
schema
using@sinclair/typebox
to specify the expected type for each variable. dotenv: true
tells the plugin to load the.env
file (requiresdotenv
package).confKey: 'config'
makes the loaded variables accessible viafastify.config
.
- We define a
-
Create Vonage Service (
src/services/vonage.js
): This module initializes the Vonage SDK and provides a function to send SMS messages. Encapsulating this logic improves organization and testability.// src/services/vonage.js import { Vonage } from '@vonage/server-sdk'; import path from 'path'; import { promises as fs } from 'fs'; let vonage; // Function to initialize Vonage SDK asynchronously async function initializeVonage(config, log) { if (vonage) { return vonage; } try { let privateKeyContent; const keyPathOrContent = config.VONAGE_PRIVATE_KEY_PATH; // Check if it looks like a file path or the key content itself if (keyPathOrContent.startsWith('-----BEGIN PRIVATE KEY-----')) { log.info('Using private key content directly from environment variable.'); privateKeyContent = keyPathOrContent; } else { log.info(`Attempting to read private key file from path: ${keyPathOrContent}`); // Construct the absolute path to the private key file const privateKeyPath = path.resolve(process.cwd(), keyPathOrContent); // Check if the private key file exists before reading try { await fs.access(privateKeyPath); // Read the private key content // The SDK expects the key content as a string or buffer privateKeyContent = await fs.readFile(privateKeyPath); log.info(`Successfully read private key file from: ${privateKeyPath}`); } catch (err) { log.error({ err }, `Vonage Private Key file access error at path: ${privateKeyPath}`); throw new Error(`Vonage Private Key file not found or inaccessible at path specified in VONAGE_PRIVATE_KEY_PATH: ${keyPathOrContent}`); } } if (!privateKeyContent) { throw new Error('Vonage Private Key content could not be determined.'); } vonage = new Vonage({ apiKey: config.VONAGE_API_KEY, apiSecret: config.VONAGE_API_SECRET, applicationId: config.VONAGE_APPLICATION_ID, privateKey: privateKeyContent, // Pass the key content directly }); log.info('Vonage SDK initialized successfully.'); return vonage; } catch (error) { log.error('Failed to initialize Vonage SDK:', error); // Throwing error to prevent app from starting with invalid config throw new Error(`Vonage SDK initialization failed: ${error.message}`); } } // Function to send SMS using the Messages API async function sendSms(to, text, config, log) { const vonageInstance = await initializeVonage(config, log); // Ensure SDK is initialized try { const resp = await vonageInstance.messages.send({ message_type: 'text', to: to, from: config.VONAGE_SMS_FROM, channel: 'sms', text: text, }); log.info(`SMS sent successfully to ${to}. Message UUID: ${resp.message_uuid}`); return resp; // Contains message_uuid } catch (err) { // Log detailed error information from Vonage if available const errorDetails = err?.response?.data || { message: err.message, detail: err.detail }; log.error({ message: `Error sending SMS to ${to}`, vonageError: errorDetails, statusCode: err?.response?.status }, `Vonage API Error: ${errorDetails.title || errorDetails.message || 'Unknown error'}`); // Rethrow a structured error for the route handler throw new Error(`Vonage API Error: ${errorDetails.title || errorDetails.detail || errorDetails.message || 'Unknown error sending SMS'}`); } } export { initializeVonage, sendSms };
initializeVonage
: Now checks ifVONAGE_PRIVATE_KEY_PATH
contains the key content directly or a file path. It reads the file only if it looks like a path. Logs the original error (err
) if file access fails.sendSms
: Takes the recipient number (to
), message text, config, and logger. It callsvonageInstance.messages.send
with the required parameters for the Messages API SMS channel. Enhanced error logging captures more details from the Vonage SDK error response.path.resolve
andprocess.cwd()
ensure the private key path is handled correctly regardless of where the script is run from.
-
Create SMS Route (
src/routes/sms.js
): This file defines the/send-sms
endpoint.// src/routes/sms.js import { Type } from '@sinclair/typebox'; import { sendSms as sendVonageSms } from '../services/vonage.js'; // Schema for validating the request body const sendSmsBodySchema = Type.Object({ to: Type.String({ description: 'Recipient phone number in E.164 format (e.g., +14155550100)', // Basic E.164 pattern check (starts with +, followed by 1-15 digits) pattern: '^\\+[1-9]\\d{1,14}$' }), message: Type.String({ description: 'The text message content', minLength: 1, maxLength: 1600 // Standard SMS limit consideration }), }, { additionalProperties: false // Disallow properties not defined in the schema }); // Schema for the success response const sendSmsResponseSchema = Type.Object({ success: Type.Boolean(), message_uuid: Type.String(), message: Type.String(), }); // Optional: Schema for error responses const errorResponseSchema = Type.Object({ success: Type.Boolean({ default: false }), message: Type.String() }); async function smsRoutes(fastify, options) { // Expose config and logger from fastify instance const { config, log } = fastify; fastify.post('/send-sms', { schema: { description: 'Sends an SMS message via Vonage Messages API', tags: ['SMS'], summary: 'Send an SMS', body: sendSmsBodySchema, response: { 200: sendSmsResponseSchema, // Define potential error responses for documentation/validation 400: errorResponseSchema, // Validation errors, potentially some Vonage errors 500: errorResponseSchema // Server/Vonage errors } } }, async (request, reply) => { const { to, message } = request.body; log.info(`Received request to send SMS to: ${to}`); try { // Call the Vonage service function const result = await sendVonageSms(to, message, config, log); reply.code(200).send({ success: true, message_uuid: result.message_uuid, message: `SMS submitted successfully to ${to}`, }); } catch (error) { // Log the error with context log.error({ err: error }, `Failed to send SMS to ${to}: ${error.message}`); // Determine appropriate status code based on error type if possible // Example: Check for specific Vonage error messages if (error.message && error.message.includes("Non-Whitelisted")) { reply.code(400).send({ success: false, message: 'Destination number not whitelisted for trial account.' }); } else if (error.message && error.message.includes("Authentication Failed")) { reply.code(401).send({ success: false, message: 'Vonage authentication failed. Check credentials.' }); } else { // Default to 500 for other errors reply.code(500).send({ success: false, message: error.message || 'An internal server error occurred while sending the SMS.', }); } } }); // Basic health check endpoint fastify.get('/health', { schema: { description: 'Checks the health of the service', tags: ['Health'], summary: 'Health Check', response: { 200: Type.Object({ status: Type.String(), timestamp: Type.String({ format: 'date-time' }) }) } } }, async (request, reply) => { return { status: 'ok', timestamp: new Date().toISOString() }; }); } export default smsRoutes;
- Schema Validation: Corrected the regex pattern for
to
to^\\+[1-9]\\d{1,14}$
. AddedadditionalProperties: false
for stricter validation. Included optional error response schemas. - Route Handler: Improved error handling to potentially return 400 or 401 based on specific Vonage error messages.
- Health Check: Added basic schema definition.
- Schema Validation: Corrected the regex pattern for
-
Set up Fastify Application (
src/app.js
): This file creates the Fastify instance, registers plugins and routes.// src/app.js import Fastify from 'fastify'; import configLoader from './plugins/env.js'; import smsRoutes from './routes/sms.js'; import { initializeVonage } from './services/vonage.js'; import fastifyRateLimit from 'fastify-rate-limit'; async function buildApp() { const fastify = Fastify({ logger: { level: process.env.LOG_LEVEL || 'info', // Use env var or default // Use pino-pretty only if not in production for better performance transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, } : undefined, // Use default JSON output in production }, }); // Register environment variable plugin FIRST await fastify.register(configLoader); // Initialize Vonage SDK after config is loaded // Ensures config is available and handles potential async init errors on startup try { await initializeVonage(fastify.config, fastify.log); } catch (err) { // Use console.error as logger might not be fully ready if init fails early console.error("Critical error during Vonage SDK initialization. Exiting.", err); process.exit(1); // Exit if critical setup fails } // Register basic rate limiting // Consider making max/timeWindow configurable via env vars await fastify.register(fastifyRateLimit, { max: 100, // Max requests per window per IP timeWindow: '1 minute', // Time window // keyGenerator: function (req) { /* more sophisticated key */ return req.ip } }); // Register routes with a prefix await fastify.register(smsRoutes, { prefix: '/api/v1' }); fastify.log.info('Application setup complete. Routes registered under /api/v1'); return fastify; } export default buildApp;
- Initializes Fastify with logging. Uses
pino-pretty
conditionally (only outside production). - Registers the
configLoader
plugin. - Calls
initializeVonage
after the config is loaded to ensure the SDK is ready and catch initialization errors early. Usesconsole.error
in the catch block as the logger might fail if the error is very early. - Registers
fastify-rate-limit
. - Registers the
smsRoutes
under the/api/v1
prefix.
- Initializes Fastify with logging. Uses
-
Create Server Entry Point (
server.js
): This file imports the app builder and starts the server.// server.js import buildApp from './src/app.js'; async function start() { let fastify; try { fastify = await buildApp(); // Use config loaded by the app const port = fastify.config.PORT; const host = fastify.config.HOST; await fastify.listen({ port, host }); // Logger is available after listen resolves successfully fastify.log.info(`Server listening on http://${host}:${port}`); fastify.log.info(`SMS Send Endpoint: POST http://${host}:${port}/api/v1/send-sms`); fastify.log.info(`Health Check Endpoint: GET http://${host}:${port}/api/v1/health`); } catch (err) { // Use console.error for reliable logging during startup failures console.error('Error starting server:', err); // Attempt to log with Fastify logger if available, otherwise console is fallback if (fastify && fastify.log) { fastify.log.error(err); } process.exit(1); } } start();
- Calls
buildApp
. - Retrieves
PORT
andHOST
fromfastify.config
. - Starts the server using
fastify.listen()
. - Includes robust error handling for server startup, using
console.error
as a fallback.
- Calls
-
Add/Update Scripts in
package.json
: Modify thescripts
section in yourpackage.json
:{ "scripts": { "start": "node server.js", "dev": "NODE_ENV=development node server.js", "test": "echo \"Error: no test specified\" && exit 1" } }
npm start
: Runs the server (intended for production, uses default JSON logging).npm run dev
: Runs the server in development mode usingpino-pretty
for formatted logs (requirespino-pretty
installed as a dev dependency). SettingNODE_ENV=development
helpsapp.js
choose the right logger transport.
4. Verification and Testing
Now let's test the endpoint.
-
Ensure
.env
is Populated: Double-check that your.env
file contains the correct Vonage credentials obtained in Section 2. Remember to add the target phone number to the Test Numbers list in Vonage if using a trial account. -
Start the Server (Development Mode):
npm run dev
You should see output indicating the server is listening, similar to (with pretty formatting):
INFO: Server listening on http://0.0.0.0:3000 INFO: SMS Send Endpoint: POST http://0.0.0.0:3000/api/v1/send-sms INFO: Health Check Endpoint: GET http://0.0.0.0:3000/api/v1/health
-
Send a Test Request (using cURL): Open another terminal window. Replace
+1YOUR_TEST_PHONE_NUMBER
with a verified test number (including the+
and country code) and adjust the message. Note the/api/v1
prefix in the URL.curl -X POST http://localhost:3000/api/v1/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+1YOUR_TEST_PHONE_NUMBER"", ""message"": ""Hello from Fastify and Vonage! Sent at: '""$(date)""'"" }'
-
Check the Response:
-
Success: If the request is successful, you should receive a JSON response like this (HTTP Status 200), and an SMS should arrive on the target phone shortly after.
{ ""success"": true, ""message_uuid"": ""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"", ""message"": ""SMS submitted successfully to +1YOUR_TEST_PHONE_NUMBER"" }
You should also see log output in the server terminal confirming the request and successful submission.
-
Failure: If there's an error (e.g., invalid credentials, non-whitelisted number), you'll get an error response (HTTP Status 4xx or 5xx):
// Example: Non-whitelisted number error (HTTP 400) { ""success"": false, ""message"": ""Destination number not whitelisted for trial account."" }
Check the server logs for more detailed error information.
-
-
Test Validation: Try sending invalid requests (Note the
/api/v1
prefix):-
Missing
to
ormessage
:curl -X POST http://localhost:3000/api/v1/send-sms -H ""Content-Type: application/json"" -d '{""message"": ""test""}'
(Expected Response: 400 Bad Request with validation error like
body should have required property 'to'
) -
Incorrect phone number format (missing
+
or has letters):curl -X POST http://localhost:3000/api/v1/send-sms -H ""Content-Type: application/json"" -d '{""to"": ""12345abc"", ""message"": ""test""}'
(Expected Response: 400 Bad Request with validation error like
body/to must match pattern ""^\\+[1-9]\\d{1,14}$""
) -
Extra invalid property:
curl -X POST http://localhost:3000/api/v1/send-sms -H ""Content-Type: application/json"" -d '{""to"": ""+14155550100"", ""message"": ""test"", ""extraField"": 123}'
(Expected Response: 400 Bad Request with validation error like
body must NOT have additional properties
)
-
-
Check Health Endpoint: (Note the
/api/v1
prefix)curl http://localhost:3000/api/v1/health
(Expected Response:
{""status"":""ok"",""timestamp"":""...""}
with HTTP Status 200)
5. Error Handling and Logging
- Logging: We've configured Fastify's built-in Pino logger. Logs provide information on requests, successful sends, and errors. The
dev
script usespino-pretty
for readability. In production (npm start
), logs are typically JSON formatted for easier parsing by log management systems. Check the terminal where the server is running. - Error Handling:
- Validation Errors: Fastify automatically handles request schema validation errors, returning detailed 400 Bad Request responses.
- Vonage API Errors: The
sendSms
service catches errors from the Vonage SDK, logs detailed information, and throws a descriptive error. The route handler catches this, logs it again (with request context), and attempts to return an appropriate HTTP status code (e.g., 400 for whitelisting, 401 for auth, 500 for others). - Initialization Errors: Errors during Vonage SDK initialization in
app.js
(e.g., invalid key path) will cause the application to log the critical error toconsole.error
and exit, preventing it from running in a broken state.
- Retry Mechanisms: For simple SMS sending triggered by an API call, retries are often best handled by the client calling this API. If this service were part of a larger asynchronous workflow (e.g., processing a job queue), you might implement retries with exponential backoff within the service for transient Vonage API errors (like 429 Too Many Requests or temporary 5xx errors). Libraries like
async-retry
could be integrated into thesendSms
function if needed.
6. Security Features
- Input Validation: Implemented via Fastify's schema validation (
@sinclair/typebox
) on the/send-sms
route. This prevents malformed requests, enforces formats (like E.164), and rejects unexpected fields (additionalProperties: false
). - Rate Limiting: Basic IP-based rate limiting is added using
fastify-rate-limit
to mitigate simple DoS attacks or accidental abuse. Configure themax
andtimeWindow
parameters based on expected usage and security requirements. For production, consider more sophisticated rate limiting (e.g., per API key if you add authentication) or integrating with API gateways. - Secret Management: API keys, secrets, and the private key path/content are managed via environment variables and the
.env
file (excluded from Git). In production, use secure environment variable injection methods provided by your hosting platform or secret management tools (like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager). Avoid hardcoding credentials directly in the source code. - Private Key Handling: The service logic (
src/services/vonage.js
) supports reading the private key either from a file path specified in.env
or directly from the environment variable content itself. Reading from a file is generally preferred for local development, while injecting the key content directly into the environment variable is often more secure and practical in containerized/cloud environments. Ensure the key file (if used) has restricted file permissions and is never committed to version control.