• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to securely integrate GitHub API repositories endpoints into WordPress custom plugins using Heartbeat API

How to securely integrate GitHub API repositories endpoints into WordPress custom plugins using Heartbeat API

Securing GitHub API Access within WordPress via Heartbeat

Integrating external APIs into WordPress, especially for sensitive operations like managing code repositories, demands a robust security posture. This guide details a production-ready approach to securely fetch and display GitHub repository data within a custom WordPress plugin by leveraging the WordPress Heartbeat API for real-time, secure communication. This method avoids exposing API credentials directly in client-side JavaScript and ensures data is fetched server-side.

Prerequisites and Setup

Before diving into the code, ensure you have:

  • A GitHub Personal Access Token (PAT) with appropriate repository read permissions (e.g., repo scope). Store this token securely, ideally in environment variables or a secure configuration management system, not directly in your plugin’s code.
  • A WordPress development environment.
  • A basic understanding of WordPress plugin development and PHP.

Plugin Structure and Core Components

We’ll create a simple plugin with the following structure:

  • github-api-integration/ (Plugin directory)
  • github-api-integration.php (Main plugin file)
  • includes/ (Directory for helper classes/functions)
  • includes/class-github-api-client.php (Handles GitHub API requests)
  • includes/class-github-heartbeat-handler.php (Manages Heartbeat API interactions)
  • assets/js/github-repo-fetcher.js (Client-side JavaScript to trigger Heartbeat and display data)

Secure GitHub API Client (Server-Side)

The GitHub_API_Client class will encapsulate all interactions with the GitHub API. It’s crucial to fetch the PAT from a secure source. For this example, we’ll simulate fetching it from an environment variable, which is a common and recommended practice in production deployments.

includes/class-github-api-client.php

This class uses WordPress’s HTTP API for making requests, ensuring proper handling of redirects, cookies, and security. The GitHub PAT is passed as an `Authorization` header.

<?php
/**
 * Handles secure communication with the GitHub API.
 */
class GitHub_API_Client {

    private $api_base_url = 'https://api.github.com';
    private $pat;

    public function __construct() {
        // In a real-world scenario, fetch this from environment variables
        // or a secure WordPress option, NOT hardcoded.
        // Example: $this->pat = getenv('GITHUB_PAT');
        // For demonstration, we'll use a placeholder. Replace with your actual PAT retrieval.
        $this->pat = defined('GITHUB_PAT') ? GITHUB_PAT : ''; // Assuming GITHUB_PAT is defined elsewhere securely.

        if ( empty( $this->pat ) ) {
            error_log( 'GitHub PAT is not configured. GitHub API requests will fail.' );
        }
    }

    /**
     * Fetches repositories for a given GitHub user.
     *
     * @param string $username The GitHub username.
     * @return array|WP_Error An array of repositories or a WP_Error object on failure.
     */
    public function get_user_repositories( $username ) {
        if ( empty( $this->pat ) ) {
            return new WP_Error( 'github_api_error', __( 'GitHub Personal Access Token is missing.', 'github-api-integration' ) );
        }

        $url = trailingslashit( $this->api_base_url ) . 'users/' . urlencode( $username ) . '/repos';
        $args = array(
            'headers' => array(
                'Authorization' => 'token ' . $this->pat,
                'Accept'        => 'application/vnd.github.v3+json',
            ),
            'timeout' => 15, // Adjust timeout as needed
        );

        $response = wp_remote_get( $url, $args );

        if ( is_wp_error( $response ) ) {
            error_log( 'GitHub API Error (wp_remote_get): ' . $response->get_error_message() );
            return $response;
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );
        $status_code = wp_remote_retrieve_response_code( $response );

        if ( $status_code !== 200 ) {
            $error_message = isset( $data['message'] ) ? $data['message'] : 'Unknown GitHub API error.';
            error_log( sprintf( 'GitHub API Error: Status %d, Message: %s', $status_code, $error_message ) );
            return new WP_Error( 'github_api_error', $error_message, array( 'status' => $status_code ) );
        }

        // Basic sanitization: ensure we only return expected fields
        $sanitized_repos = array();
        if ( is_array( $data ) ) {
            foreach ( $data as $repo ) {
                $sanitized_repos[] = array(
                    'name'        => sanitize_text_field( $repo['name'] ),
                    'description' => sanitize_textarea_field( $repo['description'] ),
                    'html_url'    => esc_url_raw( $repo['html_url'] ),
                    'stargazers_count' => intval( $repo['stargazers_count'] ),
                    'forks_count' => intval( $repo['forks_count'] ),
                );
            }
        }

        return $sanitized_repos;
    }
}
?>

Heartbeat API Handler (Server-Side)

The WordPress Heartbeat API allows for frequent, non-intrusive AJAX requests from the browser to the server. We’ll hook into this to trigger our GitHub API calls and return the data to the client.

includes/class-github-heartbeat-handler.php

<?php
/**
 * Handles Heartbeat API interactions for fetching GitHub data.
 */
class GitHub_Heartbeat_Handler {

    private $github_client;
    private $github_username; // The GitHub username to fetch repos for.

    public function __construct( $github_username ) {
        $this->github_client = new GitHub_API_Client();
        $this->github_username = $github_username;

        // Hook into the WordPress Heartbeat API
        add_filter( 'heartbeat_received', array( $this, 'handle_heartbeat' ), 10, 2 );
    }

    /**
     * Processes Heartbeat requests and returns GitHub data if requested.
     *
     * @param array $response The response data.
     * @param array $data The data sent from the client.
     * @return array The modified response data.
     */
    public function handle_heartbeat( $response, $data ) {
        // Check if our custom action is present in the Heartbeat data
        if ( isset( $data['github_action'] ) && 'fetch_repos' === $data['github_action'] ) {
            // Fetch repositories from GitHub
            $repositories = $this->github_client->get_user_repositories( $this->github_username );

            if ( is_wp_error( $repositories ) ) {
                $response['github_repos'] = array(
                    'success' => false,
                    'message' => $repositories->get_error_message(),
                );
            } else {
                $response['github_repos'] = array(
                    'success' => true,
                    'data'    => $repositories,
                );
            }
        }
        return $response;
    }

    /**
     * Enqueues the Heartbeat API script and our custom fetcher script.
     */
    public function enqueue_scripts() {
        // Ensure Heartbeat is enabled and our script is enqueued.
        // The Heartbeat API is usually enqueued by default when logged in.
        // We need to enqueue our custom script that will trigger the heartbeat.
        wp_enqueue_script(
            'github-repo-fetcher',
            plugin_dir_url( __FILE__ ) . '../assets/js/github-repo-fetcher.js',
            array( 'jquery', 'heartbeat' ), // Depends on jQuery and Heartbeat
            filemtime( plugin_dir_path( __FILE__ ) . '../assets/js/github-repo-fetcher.js' ),
            true // Load in footer
        );

        // Pass necessary data to the JavaScript file
        wp_localize_script( 'github-repo-fetcher', 'github_fetcher_params', array(
            'ajax_url'    => admin_url( 'admin-ajax.php' ), // Not strictly needed for Heartbeat, but good practice
            'heartbeat_nonce' => wp_create_nonce( 'heartbeat_nonce' ), // Nonce for Heartbeat
            'github_action' => 'fetch_repos',
            'github_username' => $this->github_username,
        ) );
    }
}
?>

Main Plugin File and Initialization

The main plugin file will instantiate our classes and hook into WordPress actions.

github-api-integration.php

<?php
/**
 * Plugin Name: GitHub API Integration
 * Description: Integrates GitHub repository data into WordPress using the Heartbeat API.
 * Version: 1.0.0
 * Author: Your Name
 * Text Domain: github-api-integration
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Define constants for secure PAT storage (example).
// In production, this should be managed via environment variables or a secure configuration.
// For local development, you might define this in wp-config.php:
// define( 'GITHUB_PAT', 'your_personal_access_token_here' );
// define( 'GITHUB_USERNAME_TO_FETCH', 'your_github_username' );

// Load required files.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-github-api-client.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/class-github-heartbeat-handler.php';

/**
 * Initialize the plugin.
 */
function github_api_integration_init() {
    // Retrieve GitHub username from a secure source.
    // Example: Use a WordPress option, environment variable, or constant.
    $github_username = defined('GITHUB_USERNAME_TO_FETCH') ? GITHUB_USERNAME_TO_FETCH : '';

    if ( empty( $github_username ) ) {
        error_log( 'GITHUB_USERNAME_TO_FETCH is not configured. GitHub integration will not work.' );
        return;
    }

    // Instantiate the Heartbeat handler.
    $heartbeat_handler = new GitHub_Heartbeat_Handler( $github_username );

    // Hook into the action to enqueue scripts.
    // This should be hooked to 'admin_enqueue_scripts' or 'wp_enqueue_scripts'
    // depending on where you want the data to be available.
    // For this example, let's assume an admin page.
    add_action( 'admin_enqueue_scripts', array( $heartbeat_handler, 'enqueue_scripts' ) );

    // If you want this on the frontend, use 'wp_enqueue_scripts' instead.
    // add_action( 'wp_enqueue_scripts', array( $heartbeat_handler, 'enqueue_scripts' ) );
}
add_action( 'plugins_loaded', 'github_api_integration_init' );

/**
 * Add settings page for GitHub username and PAT (optional but recommended).
 * This is a placeholder for a more robust settings implementation.
 */
function github_api_integration_admin_menu() {
    add_options_page(
        __( 'GitHub API Settings', 'github-api-integration' ),
        __( 'GitHub API', 'github-api-integration' ),
        'manage_options',
        'github-api-settings',
        'github_api_integration_settings_page'
    );
    add_action( 'admin_init', 'github_api_integration_settings_init' );
}
add_action( 'admin_menu', 'github_api_integration_admin_menu' );

function github_api_integration_settings_init() {
    register_setting( 'github_api_settings_group', 'github_pat_setting', array(
        'type' => 'string',
        'sanitize_callback' => 'sanitize_text_field',
        'default' => '',
    ) );
    register_setting( 'github_api_settings_group', 'github_username_setting', array(
        'type' => 'string',
        'sanitize_callback' => 'sanitize_text_field',
        'default' => '',
    ) );

    add_settings_section(
        'github_api_section',
        __( 'GitHub API Configuration', 'github-api-integration' ),
        'github_api_section_callback',
        'github-api-settings'
    );

    add_settings_field(
        'github_username_field',
        __( 'GitHub Username', 'github-api-integration' ),
        'github_username_field_callback',
        'github-api-settings',
        'github_api_section'
    );

    add_settings_field(
        'github_pat_field',
        __( 'GitHub Personal Access Token (PAT)', 'github-api-integration' ),
        'github_pat_field_callback',
        'github-api-settings',
        'github_api_section'
    );
}

function github_api_section_callback() {
    echo '

' . __( 'Enter your GitHub username and Personal Access Token. The PAT should have at least "repo" scope for private repositories, or no scope for public ones.', 'github-api-integration' ) . '

'; } function github_username_field_callback() { $username = get_option( 'github_username_setting', '' ); echo '<input type="text" name="github_username_setting" value="' . esc_attr( $username ) . '" class="regular-text" />'; } function github_pat_field_callback() { $pat = get_option( 'github_pat_setting', '' ); echo '<input type="password" name="github_pat_setting" value="' . esc_attr( $pat ) . '" class="regular-text" />'; echo '<p class="description">' . __( 'Store your PAT securely. It will be used server-side only.', 'github-api-integration' ) . '</p>'; } function github_api_integration_settings_page() { ?> <div class="wrap"> <h1><?php _e( 'GitHub API Settings', 'github-api-integration' ); ?></h1> <form action="options.php" method="post"> <?php settings_fields( 'github_api_settings_group' ); do_settings_sections( 'github-api-settings' ); submit_button(); ?> </form> </div> <?php } // Override the PAT and Username retrieval if settings are available. // This is a simplified approach. A more robust solution would involve // checking if settings exist and then using them, potentially falling back // to constants or environment variables if settings are not yet configured. function get_github_pat_from_settings() { return get_option( 'github_pat_setting', '' ); } function get_github_username_from_settings() { return get_option( 'github_username_setting', '' ); } // Modify the GitHub_API_Client constructor to use settings if available. // This requires a slight modification to the GitHub_API_Client class. // For simplicity here, we'll assume the constants/env vars are set or // the settings are directly used by the Heartbeat handler. // A better approach: Pass the PAT and username to the constructor of GitHub_API_Client. // Let's adjust the init function to pass settings. function github_api_integration_init_with_settings() { $github_username = get_github_username_from_settings(); $github_pat = get_github_pat_from_settings(); if ( empty( $github_username ) ) { error_log( 'GitHub Username is not configured in settings. GitHub integration will not work.' ); return; } if ( empty( $github_pat ) ) { error_log( 'GitHub PAT is not configured in settings. GitHub API requests will fail.' ); // Decide if you want to proceed with public repos or stop. // For this example, we'll proceed but the client will log an error. } // Instantiate the Heartbeat handler, passing username and PAT. // We need to modify GitHub_Heartbeat_Handler and GitHub_API_Client to accept these. // For now, let's assume the GitHub_API_Client can access settings directly or via constants. // A cleaner way: // $github_client = new GitHub_API_Client( $github_pat ); // $heartbeat_handler = new GitHub_Heartbeat_Handler( $github_username, $github_client ); // Let's stick to the original structure for clarity, assuming PAT/Username are globally accessible or defined. // The settings page provides a UI to set these, which then should be used. // The `defined('GITHUB_PAT')` check in the client is a placeholder. // A better approach for the client: /* class GitHub_API_Client { // ... public function __construct( $pat = null ) { if ( $pat ) { $this->pat = $pat; } else { // Fallback to constants/env vars/options $this->pat = defined('GITHUB_PAT') ? GITHUB_PAT : get_option('github_pat_setting', ''); } // ... } } */ // For this example, we'll assume the settings are read by the client directly. // The `github_api_integration_init` function is called via `plugins_loaded`. // The `enqueue_scripts` method is called via `admin_enqueue_scripts`. // This ensures settings are available when the client tries to access them. // Re-instantiate with the assumption that the client can read settings. $heartbeat_handler = new GitHub_Heartbeat_Handler( $github_username ); add_action( 'admin_enqueue_scripts', array( $heartbeat_handler, 'enqueue_scripts' ) ); } // Replace the previous init hook with this one that considers settings. remove_action( 'plugins_loaded', 'github_api_integration_init' ); add_action( 'plugins_loaded', 'github_api_integration_init_with_settings' ); ?>

Client-Side JavaScript for Heartbeat Trigger

This JavaScript file will use jQuery and the WordPress Heartbeat API to send a request to the server and then display the received GitHub repository data.

assets/js/github-repo-fetcher.js

jQuery(document).ready(function($) {

    // Check if Heartbeat API is available and our parameters are set.
    if (typeof wp !== 'undefined' && typeof wp.heartbeat !== 'undefined' && typeof github_fetcher_params !== 'undefined') {

        // Initialize Heartbeat with custom settings.
        // The interval is set to 60 seconds (default is 15-60 seconds).
        // We can set it lower if we need more frequent updates, but be mindful of server load.
        wp.heartbeat.interval( github_fetcher_params.heartbeat_interval || 60000 ); // Default 60 seconds

        // Start Heartbeat.
        wp.heartbeat.connect();

        // Handle Heartbeat responses.
        $(document).on('heartbeat-send', function(e, data) {
            // Add our custom action to the data being sent.
            data.github_action = github_fetcher_params.github_action;
            // Optionally, send the username if the server needs it explicitly.
            // data.github_username = github_fetcher_params.github_username;
        });

        // Handle Heartbeat responses.
        $(document).on('heartbeat-tick', function(e, data) {
            // Check if our custom data is in the response.
            if (data.github_repos) {
                var response = data.github_repos;

                if (response.success) {
                    // Display the repositories.
                    displayRepositories(response.data);
                } else {
                    // Display error message.
                    displayError(response.message);
                }
            }
        });

        // Handle Heartbeat connection errors.
        $(document).on('heartbeat-error', function(e, error) {
            console.error('Heartbeat Error:', error);
            displayError('Could not connect to the server to fetch GitHub data.');
        });

        /**
         * Displays the fetched GitHub repositories on the page.
         * @param {Array} repos - Array of repository objects.
         */
        function displayRepositories(repos) {
            var $container = $('#github-repos-container');
            if ($container.length === 0) {
                console.warn('GitHub repos container not found.');
                return;
            }

            $container.empty(); // Clear previous content

            if (repos.length === 0) {
                $container.html('<p>No repositories found.</p>');
                return;
            }

            var html = '<ul>';
            repos.forEach(function(repo) {
                html += '<li>';
                html += '<h3><a href="' + repo.html_url + '" target="_blank" rel="noopener noreferrer">' + repo.name + '</a></h3>';
                if (repo.description) {
                    html += '<p>' + escapeHtml(repo.description) + '</p>';
                }
                html += '<p><span>&#9733;</span> ' + repo.stargazers_count + ' Stars | <span>&#10006;</span> ' + repo.forks_count + ' Forks</p>';
                html += '</li>';
            });
            html += '</ul>';

            $container.html(html);
        }

        /**
         * Displays an error message.
         * @param {string} message - The error message.
         */
        function displayError(message) {
            var $container = $('#github-repos-container');
            if ($container.length === 0) {
                console.warn('GitHub repos container not found for error display.');
                return;
            }
            $container.html('<p style="color: red;">Error: ' + escapeHtml(message) + '</p>');
        }

        /**
         * Basic HTML escaping function.
         * @param {string} unsafe - The string to escape.
         * @returns {string} The escaped string.
         */
        function escapeHtml(unsafe) {
            return unsafe
                 .replace(/&/g, "&")
                 .replace(/</g, "<")
                 .replace(/>/g, ">")
                 .replace(/"/g, """)
                 .replace(/'/g, "'");
         }

        // Initial display attempt if the container exists on page load.
        // This might be useful if data is cached or if the first heartbeat tick is slow.
        // However, relying on heartbeat-tick is generally more reliable for real-time data.
        // If you need to display something immediately, you might fetch data via a standard AJAX call on load.
    } else {
        console.warn('WordPress Heartbeat API or github_fetcher_params not available.');
    }
});

Displaying the Data

To display the fetched repositories, you need to add a container element to your WordPress page (e.g., in a shortcode, a widget, or directly in a template file) and ensure the JavaScript is enqueued on that page.

Example of a shortcode that could be used:

<?php
/**
 * Shortcode to display GitHub repositories.
 */
function github_repos_shortcode() {
    // Ensure the script is enqueued if this shortcode is used.
    // This is a basic way; a more robust plugin would manage enqueuing more strategically.
    // The enqueue_scripts method in GitHub_Heartbeat_Handler is already hooked.
    // We just need to ensure the shortcode is rendered on a page where scripts are loaded.

    ob_start();
    ?>
    <div id="github-repos-container">
        <p><?php _e( 'Loading GitHub repositories...', 'github-api-integration' ); ?></p>
    </div>
    <?php
    return ob_get_clean();
}
add_shortcode( 'github_repos', 'github_repos_shortcode' );
?>

Security Considerations and Best Practices

  • Personal Access Token (PAT) Security: Never hardcode your PAT. Use environment variables, secure WordPress options (encrypted if possible), or a secrets management system. The provided settings page is a basic example; for higher security, consider more advanced methods.
  • Scope of PAT: Grant only the necessary permissions to your PAT. For reading public repositories, no scope might be needed. For private repositories, the repo scope is typically required.
  • Rate Limiting: Be aware of GitHub’s API rate limits. The Heartbeat API’s frequency can contribute to hitting these limits if not managed carefully. Adjust the Heartbeat interval (e.g., to 60 seconds or more) and consider caching API responses server-side if data doesn’t need to be real-time.
  • Input Sanitization and Output Escaping: Always sanitize any user input (like the GitHub username) and escape all output to prevent XSS vulnerabilities. The example code includes basic sanitization and escaping.
  • Error Handling: Implement comprehensive error handling on both the server and client sides to gracefully manage API failures or network issues.
  • Nonce Verification: While Heartbeat uses its own internal mechanisms, for custom AJAX actions, always use nonces to verify requests. The `heartbeat_nonce` is passed for potential future use or if you switch to standard AJAX.
  • Server-Side Logic: Keep sensitive operations and API credential handling on the server. The Heartbeat API facilitates this by acting as a bridge between the client and server.

Deployment and Configuration

To deploy this plugin:

  1. Place the plugin files in wp-content/plugins/github-api-integration/.
  2. Activate the plugin through the WordPress admin dashboard.
  3. Navigate to Settings > GitHub API.
  4. Enter your GitHub username and your Personal Access Token. Save the settings.
  5. Ensure the `GITHUB_PAT` and `GITHUB_USERNAME_TO_FETCH` constants are defined securely in your wp-config.php or via environment variables if you are not using the settings page. The provided code prioritizes settings but has fallbacks.
  6. Add the shortcode [github_repos] to any page or post where you want to display the repositories.
  7. Make sure the page where the shortcode is used is loaded in an environment where Heartbeat is active (typically logged-in users in the WordPress admin area, or if enabled on the frontend).

Conclusion

By integrating GitHub API calls through the WordPress Heartbeat API, you achieve a secure and dynamic way to display repository information within your WordPress site. This approach centralizes API credential management server-side, minimizes exposure, and allows for near real-time updates without compromising security. Remember to adapt the PAT retrieval and Heartbeat frequency to your specific security and performance requirements.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala